| import axios from "axios"; |
| import { useCommonStore } from "@/stores/common"; |
| import { useI18n, useModuleI18n } from "@/i18n/composables"; |
| import { getPlatformDisplayName } from "@/utils/platformUtils"; |
| import { resolveErrorMessage } from "@/utils/errorUtils"; |
| import { |
| buildSearchQuery, |
| matchesPluginSearch, |
| normalizeStr, |
| toInitials, |
| toPinyinText, |
| } from "@/utils/pluginSearch"; |
| import { |
| getValidHashTab, |
| replaceTabRoute, |
| } from "@/utils/hashRouteTabs.mjs"; |
| import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue"; |
| import { useRoute, useRouter } from "vue-router"; |
| import { useDisplay } from "vuetify"; |
|
|
| const useRandomPluginsDisplay = ({ activeTab, marketSearch, currentPage }) => { |
| const showRandomPlugins = ref(true); |
|
|
| const toggleRandomPluginsVisibility = () => { |
| showRandomPlugins.value = !showRandomPlugins.value; |
| }; |
|
|
| const collapseRandomPlugins = () => { |
| showRandomPlugins.value = false; |
| }; |
|
|
| watch(marketSearch, () => { |
| if (activeTab.value === "market") { |
| collapseRandomPlugins(); |
| } |
| }); |
|
|
| watch(currentPage, (newPage, oldPage) => { |
| if (newPage === oldPage) return; |
| if (activeTab.value !== "market") return; |
| collapseRandomPlugins(); |
| }); |
|
|
| return { |
| showRandomPlugins, |
| toggleRandomPluginsVisibility, |
| collapseRandomPlugins, |
| }; |
| }; |
|
|
| const buildFailedPluginItems = (raw) => { |
| return Object.entries(raw || {}).map(([dirName, info]) => { |
| const detail = info && typeof info === "object" ? info : {}; |
| return { |
| ...detail, |
| dir_name: dirName, |
| name: detail.name || dirName, |
| display_name: detail.display_name || detail.name || dirName, |
| error: detail.error || "", |
| traceback: detail.traceback || "", |
| reserved: !!detail.reserved, |
| }; |
| }); |
| }; |
|
|
| export const useExtensionPage = () => { |
| |
| |
| const commonStore = useCommonStore(); |
| const { t } = useI18n(); |
| const { tm } = useModuleI18n("features/extension"); |
| const router = useRouter(); |
| const route = useRoute(); |
| const { width } = useDisplay(); |
| |
| const getSelectedGitHubProxy = () => { |
| if (typeof window === "undefined" || !window.localStorage) return ""; |
| return localStorage.getItem("githubProxyRadioValue") === "1" |
| ? localStorage.getItem("selectedGitHubProxy") || "" |
| : ""; |
| }; |
| |
| |
| const conflictDialog = reactive({ |
| show: false, |
| count: 0, |
| }); |
| const checkAndPromptConflicts = async () => { |
| try { |
| const res = await axios.get("/api/commands"); |
| if (res.data.status === "ok") { |
| const conflicts = res.data.data.summary?.conflicts || 0; |
| if (conflicts > 0) { |
| conflictDialog.count = conflicts; |
| conflictDialog.show = true; |
| } |
| } |
| } catch (err) { |
| console.debug("Failed to check command conflicts:", err); |
| } |
| }; |
| const handleConflictConfirm = () => { |
| activeTab.value = "commands"; |
| }; |
| |
| const fileInput = ref(null); |
| const activeTab = ref("installed"); |
| const validTabs = ["installed", "market", "mcp", "skills", "components"]; |
| const isValidTab = (tab) => validTabs.includes(tab); |
| const getLocationHash = () => route.hash || ""; |
| const extractTabFromHash = (hash) => getValidHashTab(hash, validTabs); |
| const syncTabFromHash = (hash) => { |
| const tab = extractTabFromHash(hash); |
| if (tab) { |
| activeTab.value = tab; |
| return true; |
| } |
| return false; |
| }; |
| const extension_data = reactive({ |
| data: [], |
| message: "", |
| }); |
| |
| |
| const getInitialShowReserved = () => { |
| if (typeof window !== "undefined" && window.localStorage) { |
| const saved = localStorage.getItem("showReservedPlugins"); |
| return saved === "true"; |
| } |
| return false; |
| }; |
| const showReserved = ref(getInitialShowReserved()); |
| const snack_message = ref(""); |
| const snack_show = ref(false); |
| const snack_success = ref("success"); |
| const configDialog = ref(false); |
| const extension_config = reactive({ |
| metadata: {}, |
| config: {}, |
| }); |
| const pluginMarketData = ref([]); |
| const loadingDialog = reactive({ |
| show: false, |
| title: "", |
| statusCode: 0, |
| result: "", |
| }); |
| const showPluginInfoDialog = ref(false); |
| const selectedPlugin = ref({}); |
| const curr_namespace = ref(""); |
| const updatingAll = ref(false); |
| |
| const readmeDialog = reactive({ |
| show: false, |
| pluginName: "", |
| repoUrl: null, |
| }); |
| |
| |
| const forceUpdateDialog = reactive({ |
| show: false, |
| extensionName: "", |
| }); |
| |
| |
| const updateAllConfirmDialog = reactive({ |
| show: false, |
| }); |
| |
| |
| const changelogDialog = reactive({ |
| show: false, |
| pluginName: "", |
| repoUrl: null, |
| }); |
| |
| |
| |
| const getInitialListViewMode = () => { |
| if (typeof window !== "undefined" && window.localStorage) { |
| return localStorage.getItem("pluginListViewMode") === "true"; |
| } |
| return false; |
| }; |
| const isListView = ref(getInitialListViewMode()); |
| const pluginSearch = ref(""); |
| const installedStatusFilter = ref("all"); |
| const installedSortBy = ref("default"); |
| const installedSortOrder = ref("desc"); |
| const loading_ = ref(false); |
| |
| |
| const currentPage = ref(1); |
| |
| |
| const dangerConfirmDialog = ref(false); |
| const selectedDangerPlugin = ref(null); |
| const selectedMarketInstallPlugin = ref(null); |
| const installCompat = reactive({ |
| checked: false, |
| compatible: true, |
| message: "", |
| }); |
| |
| |
| const versionCompatibilityDialog = reactive({ |
| show: false, |
| message: "", |
| }); |
| |
| |
| const showUninstallDialog = ref(false); |
| const uninstallTarget = ref(null); |
| |
| |
| const showSourceDialog = ref(false); |
| const showSourceManagerDialog = ref(false); |
| const sourceName = ref(""); |
| const sourceUrl = ref(""); |
| const customSources = ref([]); |
| const selectedSource = ref(null); |
| const showRemoveSourceDialog = ref(false); |
| const sourceToRemove = ref(null); |
| const editingSource = ref(false); |
| const originalSourceUrl = ref(""); |
| |
| |
| const extension_url = ref(""); |
| const dialog = ref(false); |
| const upload_file = ref(null); |
| const uploadTab = ref("file"); |
| const showPluginFullName = ref(false); |
| const marketSearch = ref(""); |
| const debouncedMarketSearch = ref(""); |
| const refreshingMarket = ref(false); |
| const sortBy = ref("default"); |
| const sortOrder = ref("desc"); |
| const randomPluginNames = ref([]); |
| const { |
| showRandomPlugins, |
| toggleRandomPluginsVisibility, |
| collapseRandomPlugins, |
| } = useRandomPluginsDisplay({ |
| activeTab, |
| marketSearch, |
| currentPage, |
| }); |
| |
| |
| |
| const plugin_handler_info_headers = computed(() => [ |
| { title: tm("table.headers.eventType"), key: "event_type_h" }, |
| { title: tm("table.headers.description"), key: "desc", maxWidth: "250px" }, |
| { title: tm("table.headers.specificType"), key: "type" }, |
| { title: tm("table.headers.trigger"), key: "cmd" }, |
| ]); |
|
|
| const installedSortItems = computed(() => [ |
| { title: tm("sort.default"), value: "default" }, |
| { title: tm("sort.installTime"), value: "install_time" }, |
| { title: tm("sort.name"), value: "name" }, |
| { title: tm("sort.author"), value: "author" }, |
| { title: tm("sort.updateStatus"), value: "update_status" }, |
| ]); |
|
|
| const installedSortUsesOrder = computed( |
| () => installedSortBy.value !== "default", |
| ); |
| |
| |
| const showAuthorColumn = computed(() => width.value >= 1280); |
| const pluginHeaders = computed(() => { |
| const headers = [ |
| { |
| title: tm("table.headers.name"), |
| key: "name", |
| sortable: false, |
| width: showAuthorColumn.value ? "24%" : "26%", |
| }, |
| { |
| title: tm("table.headers.description"), |
| key: "desc", |
| sortable: false, |
| width: showAuthorColumn.value ? "32%" : "36%", |
| }, |
| { |
| title: tm("table.headers.version"), |
| key: "version", |
| sortable: false, |
| width: showAuthorColumn.value ? "12%" : "14%", |
| }, |
| ]; |
|
|
| if (showAuthorColumn.value) { |
| headers.push({ |
| title: tm("table.headers.author"), |
| key: "author", |
| sortable: false, |
| width: "10%", |
| }); |
| } |
|
|
| headers.push({ |
| title: tm("table.headers.actions"), |
| key: "actions", |
| sortable: false, |
| width: showAuthorColumn.value ? "22%" : "24%", |
| }); |
|
|
| return headers; |
| }); |
| |
| |
| const filteredExtensions = computed(() => { |
| const data = Array.isArray(extension_data?.data) ? extension_data.data : []; |
| if (!showReserved.value) { |
| return data.filter((ext) => !ext.reserved); |
| } |
| return data; |
| }); |
|
|
| const compareInstalledPluginNames = (left, right) => |
| normalizeStr(left?.name ?? "").localeCompare( |
| normalizeStr(right?.name ?? ""), |
| undefined, |
| { |
| sensitivity: "base", |
| }, |
| ); |
|
|
| const compareInstalledPluginAuthors = (left, right) => |
| normalizeStr(left?.author ?? "").localeCompare( |
| normalizeStr(right?.author ?? ""), |
| undefined, |
| { sensitivity: "base" }, |
| ); |
|
|
| const getInstalledAtTimestamp = (plugin) => { |
| const parsed = Date.parse(plugin?.installed_at ?? ""); |
| return Number.isFinite(parsed) ? parsed : null; |
| }; |
|
|
| const sortInstalledPlugins = (plugins) => { |
| return plugins |
| .map((plugin, index) => ({ |
| plugin, |
| index, |
| installedAtTimestamp: getInstalledAtTimestamp(plugin), |
| })) |
| .sort((left, right) => { |
| const fallbackNameCompare = compareInstalledPluginNames( |
| left.plugin, |
| right.plugin, |
| ); |
| const fallbackResult = |
| fallbackNameCompare !== 0 ? fallbackNameCompare : left.index - right.index; |
|
|
| if (installedSortBy.value === "install_time") { |
| const leftTimestamp = left.installedAtTimestamp; |
| const rightTimestamp = right.installedAtTimestamp; |
|
|
| if (leftTimestamp == null && rightTimestamp == null) { |
| return fallbackResult; |
| } |
| if (leftTimestamp == null) { |
| return 1; |
| } |
| if (rightTimestamp == null) { |
| return -1; |
| } |
|
|
| const timeDiff = |
| installedSortOrder.value === "desc" |
| ? rightTimestamp - leftTimestamp |
| : leftTimestamp - rightTimestamp; |
| return timeDiff !== 0 ? timeDiff : fallbackResult; |
| } |
|
|
| if (installedSortBy.value === "name") { |
| const nameCompare = compareInstalledPluginNames(left.plugin, right.plugin); |
| if (nameCompare !== 0) { |
| return installedSortOrder.value === "desc" |
| ? -nameCompare |
| : nameCompare; |
| } |
| return left.index - right.index; |
| } |
|
|
| if (installedSortBy.value === "author") { |
| const authorCompare = compareInstalledPluginAuthors( |
| left.plugin, |
| right.plugin, |
| ); |
| if (authorCompare !== 0) { |
| return installedSortOrder.value === "desc" |
| ? -authorCompare |
| : authorCompare; |
| } |
| return fallbackResult; |
| } |
|
|
| if (installedSortBy.value === "update_status") { |
| const leftHasUpdate = left.plugin?.has_update ? 1 : 0; |
| const rightHasUpdate = right.plugin?.has_update ? 1 : 0; |
| const updateDiff = |
| installedSortOrder.value === "desc" |
| ? rightHasUpdate - leftHasUpdate |
| : leftHasUpdate - rightHasUpdate; |
| return updateDiff !== 0 ? updateDiff : fallbackResult; |
| } |
|
|
| return fallbackResult; |
| }) |
| .map((item) => item.plugin); |
| }; |
|
|
| |
| const filteredPlugins = computed(() => { |
| const plugins = filteredExtensions.value.filter((plugin) => { |
| if (installedStatusFilter.value === "enabled") { |
| return !!plugin.activated; |
| } |
| if (installedStatusFilter.value === "disabled") { |
| return !plugin.activated; |
| } |
| return true; |
| }); |
|
|
| const query = buildSearchQuery(pluginSearch.value); |
| const filtered = query |
| ? plugins.filter((plugin) => matchesPluginSearch(plugin, query)) |
| : plugins; |
|
|
| return sortInstalledPlugins(filtered); |
| }); |
| |
| |
| const filteredMarketPlugins = computed(() => { |
| const query = buildSearchQuery(debouncedMarketSearch.value); |
| if (!query) { |
| return pluginMarketData.value; |
| } |
|
|
| return pluginMarketData.value.filter((plugin) => |
| matchesPluginSearch(plugin, query), |
| ); |
| }); |
| |
| |
| const sortedPlugins = computed(() => { |
| let plugins = [...filteredMarketPlugins.value]; |
| |
| |
| if (sortBy.value === "stars") { |
| |
| plugins.sort((a, b) => { |
| const starsA = a.stars ?? 0; |
| const starsB = b.stars ?? 0; |
| return sortOrder.value === "desc" ? starsB - starsA : starsA - starsB; |
| }); |
| } else if (sortBy.value === "author") { |
| |
| plugins.sort((a, b) => { |
| const authorA = (a.author ?? "").toLowerCase(); |
| const authorB = (b.author ?? "").toLowerCase(); |
| const result = authorA.localeCompare(authorB); |
| return sortOrder.value === "desc" ? -result : result; |
| }); |
| } else if (sortBy.value === "updated") { |
| |
| plugins.sort((a, b) => { |
| const dateA = a.updated_at ? new Date(a.updated_at).getTime() : 0; |
| const dateB = b.updated_at ? new Date(b.updated_at).getTime() : 0; |
| return sortOrder.value === "desc" ? dateB - dateA : dateA - dateB; |
| }); |
| } else { |
| |
| const pinned = plugins.filter((plugin) => plugin?.pinned); |
| const notPinned = plugins.filter((plugin) => !plugin?.pinned); |
| return [...pinned, ...notPinned]; |
| } |
| |
| return plugins; |
| }); |
| |
| const RANDOM_PLUGINS_COUNT = 3; |
| |
| const randomPlugins = computed(() => { |
| const allPlugins = pluginMarketData.value; |
| if (allPlugins.length === 0) return []; |
| |
| const pluginsByName = new Map(allPlugins.map((plugin) => [plugin.name, plugin])); |
| const selected = randomPluginNames.value |
| .map((name) => pluginsByName.get(name)) |
| .filter(Boolean); |
| |
| if (selected.length > 0) { |
| return selected; |
| } |
| |
| return allPlugins.slice(0, Math.min(RANDOM_PLUGINS_COUNT, allPlugins.length)); |
| }); |
| |
| const shufflePlugins = (plugins) => { |
| const shuffled = [...plugins]; |
| for (let i = shuffled.length - 1; i > 0; i -= 1) { |
| const j = Math.floor(Math.random() * (i + 1)); |
| [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; |
| } |
| return shuffled; |
| }; |
| |
| const refreshRandomPlugins = () => { |
| const shuffled = shufflePlugins(pluginMarketData.value); |
| randomPluginNames.value = shuffled |
| .slice(0, Math.min(RANDOM_PLUGINS_COUNT, shuffled.length)) |
| .map((plugin) => plugin.name); |
| }; |
| |
| |
| const displayItemsPerPage = 9; |
| |
| const totalPages = computed(() => { |
| return Math.ceil(sortedPlugins.value.length / displayItemsPerPage); |
| }); |
| |
| const paginatedPlugins = computed(() => { |
| const start = (currentPage.value - 1) * displayItemsPerPage; |
| const end = start + displayItemsPerPage; |
| return sortedPlugins.value.slice(start, end); |
| }); |
| |
| const updatableExtensions = computed(() => { |
| const data = Array.isArray(extension_data?.data) ? extension_data.data : []; |
| return data.filter((ext) => ext.has_update); |
| }); |
| |
| |
| const toggleShowReserved = () => { |
| showReserved.value = !showReserved.value; |
| |
| if (typeof window !== "undefined" && window.localStorage) { |
| localStorage.setItem("showReservedPlugins", showReserved.value.toString()); |
| } |
| }; |
| |
| const toast = (message, success) => { |
| snack_message.value = message; |
| snack_show.value = true; |
| snack_success.value = success; |
| }; |
| |
| const resetLoadingDialog = () => { |
| loadingDialog.show = false; |
| loadingDialog.title = tm("dialogs.loading.title"); |
| loadingDialog.statusCode = 0; |
| loadingDialog.result = ""; |
| }; |
| |
| const onLoadingDialogResult = (statusCode, result, timeToClose = 2000) => { |
| loadingDialog.statusCode = statusCode; |
| loadingDialog.result = result; |
| if (timeToClose === -1) return; |
| setTimeout(resetLoadingDialog, timeToClose); |
| }; |
| |
| const failedPluginsDict = ref({}); |
| const failedPluginItems = computed(() => |
| buildFailedPluginItems(failedPluginsDict.value), |
| ); |
| |
| const getExtensions = async () => { |
| loading_.value = true; |
| try { |
| const res = await axios.get("/api/plugin/get"); |
| Object.assign(extension_data, res.data); |
| |
| const failRes = await axios.get("/api/plugin/source/get-failed-plugins"); |
| failedPluginsDict.value = failRes.data.data || {}; |
| |
| checkUpdate(); |
| } catch (err) { |
| toast(err, "error"); |
| } finally { |
| loading_.value = false; |
| } |
| }; |
| |
| const handleReloadAllFailed = async () => { |
| const dirNames = Object.keys(failedPluginsDict.value); |
| if (dirNames.length === 0) { |
| toast("没有需要重载的失败插件", "info"); |
| return; |
| } |
| |
| loading_.value = true; |
| try { |
| const promises = dirNames.map(dir => |
| axios.post("/api/plugin/reload-failed", { dir_name: dir }) |
| ); |
| await Promise.all(promises); |
| |
| toast("已尝试重载所有失败插件", "success"); |
| |
| |
| extension_data.message = ""; |
| |
| |
| await getExtensions(); |
| |
| } catch (e) { |
| console.error("重载失败:", e); |
| toast("批量重载过程中出现错误", "error"); |
| } finally { |
| loading_.value = false; |
| } |
| }; |
|
|
| const reloadFailedPlugin = async (dirName) => { |
| if (!dirName) return; |
|
|
| try { |
| const res = await axios.post("/api/plugin/reload-failed", { dir_name: dirName }); |
| if (res.data.status === "error") { |
| toast(res.data.message || tm("messages.reloadFailed"), "error"); |
| return; |
| } |
| toast(res.data.message || tm("messages.reloadSuccess"), "success"); |
| await getExtensions(); |
| } catch (err) { |
| toast(resolveErrorMessage(err, tm("messages.reloadFailed")), "error"); |
| } |
| }; |
|
|
| const requestUninstall = (target) => { |
| if (!target?.id || !target?.kind) return; |
| uninstallTarget.value = target; |
| showUninstallDialog.value = true; |
| }; |
|
|
| const uninstall = async ( |
| target, |
| { deleteConfig = false, deleteData = false, skipConfirm = false } = {}, |
| ) => { |
| if (!target?.id || !target?.kind) return; |
|
|
| if (!skipConfirm) { |
| requestUninstall(target); |
| return; |
| } |
|
|
| const isFailed = target.kind === "failed"; |
| const endpoint = isFailed |
| ? "/api/plugin/uninstall-failed" |
| : "/api/plugin/uninstall"; |
| const payload = isFailed |
| ? { dir_name: target.id, delete_config: deleteConfig, delete_data: deleteData } |
| : { name: target.id, delete_config: deleteConfig, delete_data: deleteData }; |
|
|
| toast(`${tm("messages.uninstalling")} ${target.id}`, "primary"); |
|
|
| try { |
| const res = await axios.post(endpoint, payload); |
| if (res.data.status === "error") { |
| toast(res.data.message, "error"); |
| return; |
| } |
| if (!isFailed) { |
| Object.assign(extension_data, res.data); |
| } |
| toast(res.data.message, "success"); |
| await getExtensions(); |
| } catch (err) { |
| toast(resolveErrorMessage(err, tm("messages.operationFailed")), "error"); |
| } |
| }; |
|
|
| const requestUninstallPlugin = (name) => { |
| if (!name) return; |
| uninstall({ kind: "normal", id: name }, { skipConfirm: false }); |
| }; |
|
|
| const requestUninstallFailedPlugin = (dirName) => { |
| if (!dirName) return; |
| uninstall({ kind: "failed", id: dirName }, { skipConfirm: false }); |
| }; |
| |
| const checkUpdate = () => { |
| const onlinePluginsMap = new Map(); |
| const onlinePluginsNameMap = new Map(); |
| |
| pluginMarketData.value.forEach((plugin) => { |
| if (plugin.repo) { |
| onlinePluginsMap.set(plugin.repo.toLowerCase(), plugin); |
| } |
| onlinePluginsNameMap.set(plugin.name, plugin); |
| }); |
| |
| const data = Array.isArray(extension_data?.data) ? extension_data.data : []; |
| data.forEach((extension) => { |
| 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 !== tm("status.unknown"); |
| } else { |
| extension.has_update = false; |
| } |
| }); |
| }; |
| |
| const uninstallExtension = async ( |
| extensionName, |
| optionsOrSkipConfirm = false, |
| ) => { |
| if (!extensionName) return; |
|
|
| if (typeof optionsOrSkipConfirm === "boolean") { |
| return uninstall( |
| { kind: "normal", id: extensionName }, |
| { skipConfirm: optionsOrSkipConfirm }, |
| ); |
| } |
|
|
| return uninstall( |
| { kind: "normal", id: extensionName }, |
| { ...(optionsOrSkipConfirm || {}), skipConfirm: true }, |
| ); |
| }; |
| |
| |
| const handleUninstallConfirm = async (options) => { |
| const target = uninstallTarget.value; |
| if (!target) return; |
|
|
| try { |
| await uninstall(target, { ...(options || {}), skipConfirm: true }); |
| } finally { |
| uninstallTarget.value = null; |
| showUninstallDialog.value = false; |
| } |
| }; |
| |
| const updateExtension = async (extension_name, forceUpdate = false) => { |
| |
| const data = Array.isArray(extension_data?.data) ? extension_data.data : []; |
| const ext = data.find((e) => e.name === extension_name); |
| |
| |
| if (!ext?.has_update && !forceUpdate) { |
| forceUpdateDialog.extensionName = extension_name; |
| forceUpdateDialog.show = true; |
| return; |
| } |
| |
| loadingDialog.title = tm("status.loading"); |
| loadingDialog.show = true; |
| try { |
| const res = await axios.post("/api/plugin/update", { |
| name: extension_name, |
| proxy: getSelectedGitHubProxy(), |
| }); |
| |
| if (res.data.status === "error") { |
| onLoadingDialogResult(2, res.data.message, -1); |
| return; |
| } |
| |
| Object.assign(extension_data, res.data); |
| onLoadingDialogResult(1, res.data.message); |
| setTimeout(async () => { |
| toast(tm("messages.refreshing"), "info", 2000); |
| try { |
| await getExtensions(); |
| toast(tm("messages.refreshSuccess"), "success"); |
| |
| |
| viewChangelog({ |
| name: extension_name, |
| repo: ext?.repo || null, |
| }); |
| } catch (error) { |
| const errorMsg = |
| error.response?.data?.message || error.message || String(error); |
| toast(`${tm("messages.refreshFailed")}: ${errorMsg}`, "error"); |
| } |
| }, 1000); |
| } catch (err) { |
| toast(err, "error"); |
| } |
| }; |
| |
| |
| |
| const showUpdateAllConfirm = () => { |
| if (updatableExtensions.value.length === 0) return; |
| updateAllConfirmDialog.show = true; |
| }; |
| |
| |
| const confirmUpdateAll = () => { |
| updateAllConfirmDialog.show = false; |
| updateAllExtensions(); |
| }; |
| |
| |
| const cancelUpdateAll = () => { |
| updateAllConfirmDialog.show = false; |
| }; |
| |
| const confirmForceUpdate = () => { |
| const name = forceUpdateDialog.extensionName; |
| forceUpdateDialog.show = false; |
| forceUpdateDialog.extensionName = ""; |
| updateExtension(name, true); |
| }; |
| |
| const updateAllExtensions = async () => { |
| if (updatingAll.value || updatableExtensions.value.length === 0) return; |
| updatingAll.value = true; |
| loadingDialog.title = tm("status.loading"); |
| loadingDialog.statusCode = 0; |
| loadingDialog.result = ""; |
| loadingDialog.show = true; |
| |
| const targets = updatableExtensions.value.map((ext) => ext.name); |
| try { |
| const res = await axios.post("/api/plugin/update-all", { |
| names: targets, |
| proxy: getSelectedGitHubProxy(), |
| }); |
| |
| if (res.data.status === "error") { |
| onLoadingDialogResult( |
| 2, |
| res.data.message || |
| tm("messages.updateAllFailed", { |
| failed: targets.length, |
| total: targets.length, |
| }), |
| -1, |
| ); |
| return; |
| } |
| |
| const results = res.data.data?.results || []; |
| const failures = results.filter((r) => r.status !== "ok"); |
| try { |
| await getExtensions(); |
| } catch (err) { |
| const errorMsg = |
| err.response?.data?.message || err.message || String(err); |
| failures.push({ name: "refresh", status: "error", message: errorMsg }); |
| } |
| |
| if (failures.length === 0) { |
| onLoadingDialogResult(1, tm("messages.updateAllSuccess")); |
| } else { |
| const failureText = tm("messages.updateAllFailed", { |
| failed: failures.length, |
| total: targets.length, |
| }); |
| const detail = failures.map((f) => `${f.name}: ${f.message}`).join("\n"); |
| onLoadingDialogResult(2, `${failureText}\n${detail}`, -1); |
| } |
| } catch (err) { |
| const errorMsg = err.response?.data?.message || err.message || String(err); |
| onLoadingDialogResult(2, errorMsg, -1); |
| } finally { |
| updatingAll.value = false; |
| } |
| }; |
| |
| const pluginOn = async (extension) => { |
| try { |
| const res = await axios.post("/api/plugin/on", { name: extension.name }); |
| if (res.data.status === "error") { |
| toast(res.data.message, "error"); |
| return; |
| } |
| toast(res.data.message, "success"); |
| await getExtensions(); |
| |
| await checkAndPromptConflicts(); |
| } catch (err) { |
| toast(err, "error"); |
| } |
| }; |
| |
| const pluginOff = async (extension) => { |
| try { |
| const res = await axios.post("/api/plugin/off", { name: extension.name }); |
| if (res.data.status === "error") { |
| toast(res.data.message, "error"); |
| return; |
| } |
| toast(res.data.message, "success"); |
| getExtensions(); |
| } catch (err) { |
| toast(err, "error"); |
| } |
| }; |
| |
| const openExtensionConfig = async (extension_name) => { |
| curr_namespace.value = extension_name; |
| configDialog.value = true; |
| try { |
| const res = await axios.get( |
| "/api/config/get?plugin_name=" + extension_name, |
| ); |
| extension_config.metadata = res.data.data.metadata; |
| extension_config.config = res.data.data.config; |
| } catch (err) { |
| toast(err, "error"); |
| } |
| }; |
| |
| const updateConfig = async () => { |
| try { |
| const res = await axios.post( |
| "/api/config/plugin/update?plugin_name=" + curr_namespace.value, |
| extension_config.config, |
| ); |
| if (res.data.status === "ok") { |
| toast(res.data.message, "success"); |
| } else { |
| toast(res.data.message, "error"); |
| } |
| configDialog.value = false; |
| extension_config.metadata = {}; |
| extension_config.config = {}; |
| getExtensions(); |
| } catch (err) { |
| toast(err, "error"); |
| } |
| }; |
| |
| const showPluginInfo = (plugin) => { |
| selectedPlugin.value = plugin; |
| showPluginInfoDialog.value = true; |
| }; |
| |
| const reloadPlugin = async (plugin_name) => { |
| try { |
| const res = await axios.post("/api/plugin/reload", { name: plugin_name }); |
| if (res.data.status === "error") { |
| toast(res.data.message || tm("messages.reloadFailed"), "error"); |
| return; |
| } |
| toast(tm("messages.reloadSuccess"), "success"); |
| await getExtensions(); |
| } catch (err) { |
| toast(resolveErrorMessage(err, tm("messages.reloadFailed")), "error"); |
| } |
| }; |
| |
| const viewReadme = (plugin) => { |
| readmeDialog.pluginName = plugin.name; |
| readmeDialog.repoUrl = plugin.repo; |
| readmeDialog.show = true; |
| }; |
| |
| |
| const viewChangelog = (plugin) => { |
| changelogDialog.pluginName = plugin.name; |
| changelogDialog.repoUrl = plugin.repo; |
| changelogDialog.show = true; |
| }; |
| |
| |
| const handleInstallPlugin = async (plugin) => { |
| if (plugin.tags && plugin.tags.includes("danger")) { |
| selectedDangerPlugin.value = plugin; |
| dangerConfirmDialog.value = true; |
| } else { |
| selectedMarketInstallPlugin.value = plugin; |
| extension_url.value = plugin.repo; |
| dialog.value = true; |
| uploadTab.value = "url"; |
| } |
| }; |
| |
| |
| const confirmDangerInstall = () => { |
| if (selectedDangerPlugin.value) { |
| selectedMarketInstallPlugin.value = selectedDangerPlugin.value; |
| extension_url.value = selectedDangerPlugin.value.repo; |
| dialog.value = true; |
| uploadTab.value = "url"; |
| } |
| dangerConfirmDialog.value = false; |
| selectedDangerPlugin.value = null; |
| }; |
| |
| |
| const cancelDangerInstall = () => { |
| dangerConfirmDialog.value = false; |
| selectedDangerPlugin.value = null; |
| }; |
| |
| |
| const loadCustomSources = async () => { |
| try { |
| const res = await axios.get("/api/plugin/source/get"); |
| if (res.data.status === "ok") { |
| customSources.value = res.data.data; |
| } else { |
| toast(res.data.message, "error"); |
| } |
| } catch (e) { |
| console.warn("Failed to load custom sources:", e); |
| customSources.value = []; |
| } |
| |
| |
| const currentSource = localStorage.getItem("selectedPluginSource"); |
| if (currentSource) { |
| selectedSource.value = currentSource; |
| } |
| }; |
| |
| const saveCustomSources = async () => { |
| try { |
| const res = await axios.post("/api/plugin/source/save", { |
| sources: customSources.value, |
| }); |
| if (res.data.status !== "ok") { |
| toast(res.data.message, "error"); |
| } |
| } catch (e) { |
| toast(e, "error"); |
| } |
| }; |
| |
| const addCustomSource = () => { |
| showSourceManagerDialog.value = false; |
| editingSource.value = false; |
| originalSourceUrl.value = ""; |
| sourceName.value = ""; |
| sourceUrl.value = ""; |
| showSourceDialog.value = true; |
| }; |
| |
| const openSourceManagerDialog = async () => { |
| await loadCustomSources(); |
| showSourceManagerDialog.value = true; |
| }; |
| |
| const selectPluginSource = (sourceUrl) => { |
| selectedSource.value = sourceUrl; |
| if (sourceUrl) { |
| localStorage.setItem("selectedPluginSource", sourceUrl); |
| } else { |
| localStorage.removeItem("selectedPluginSource"); |
| } |
| |
| refreshPluginMarket(); |
| }; |
| |
| const sourceSelectItems = computed(() => [ |
| { title: tm("market.defaultSource"), value: "__default__" }, |
| ...customSources.value.map((source) => ({ |
| title: source.name, |
| value: source.url, |
| })), |
| ]); |
| |
| const editCustomSource = (source) => { |
| if (!source) return; |
| showSourceManagerDialog.value = false; |
| editingSource.value = true; |
| originalSourceUrl.value = source.url; |
| sourceName.value = source.name; |
| sourceUrl.value = source.url; |
| showSourceDialog.value = true; |
| }; |
| |
| const removeCustomSource = (source) => { |
| if (!source) return; |
| showSourceManagerDialog.value = false; |
| sourceToRemove.value = source; |
| showRemoveSourceDialog.value = true; |
| }; |
| |
| const confirmRemoveSource = () => { |
| if (sourceToRemove.value) { |
| customSources.value = customSources.value.filter( |
| (s) => s.url !== sourceToRemove.value.url, |
| ); |
| saveCustomSources(); |
| |
| |
| if (selectedSource.value === sourceToRemove.value.url) { |
| selectedSource.value = null; |
| localStorage.removeItem("selectedPluginSource"); |
| |
| refreshPluginMarket(); |
| } |
| |
| toast(tm("market.sourceRemoved"), "success"); |
| showRemoveSourceDialog.value = false; |
| sourceToRemove.value = null; |
| } |
| }; |
| |
| const saveCustomSource = () => { |
| const normalizedUrl = sourceUrl.value.trim(); |
| |
| if (!sourceName.value.trim() || !normalizedUrl) { |
| toast(tm("messages.fillSourceNameAndUrl"), "error"); |
| return; |
| } |
| |
| |
| try { |
| new URL(normalizedUrl); |
| } catch (e) { |
| toast(tm("messages.invalidUrl"), "error"); |
| return; |
| } |
| |
| if (editingSource.value) { |
| |
| const index = customSources.value.findIndex( |
| (s) => s.url === originalSourceUrl.value, |
| ); |
| if (index !== -1) { |
| customSources.value[index] = { |
| name: sourceName.value.trim(), |
| url: normalizedUrl, |
| }; |
| |
| |
| if (selectedSource.value === originalSourceUrl.value) { |
| selectedSource.value = normalizedUrl; |
| localStorage.setItem("selectedPluginSource", selectedSource.value); |
| |
| refreshPluginMarket(); |
| } |
| } |
| } else { |
| |
| if (customSources.value.some((source) => source.url === normalizedUrl)) { |
| toast(tm("market.sourceExists"), "error"); |
| return; |
| } |
| |
| customSources.value.push({ |
| name: sourceName.value.trim(), |
| url: normalizedUrl, |
| }); |
| } |
| |
| saveCustomSources(); |
| toast( |
| editingSource.value ? tm("market.sourceUpdated") : tm("market.sourceAdded"), |
| "success", |
| ); |
| |
| |
| sourceName.value = ""; |
| sourceUrl.value = ""; |
| editingSource.value = false; |
| originalSourceUrl.value = ""; |
| showSourceDialog.value = false; |
| }; |
| |
| |
| const trimExtensionName = () => { |
| pluginMarketData.value.forEach((plugin) => { |
| if (plugin.name) { |
| let name = plugin.name.trim().toLowerCase(); |
| if (name.startsWith("astrbot_plugin_")) { |
| plugin.trimmedName = name.substring(15); |
| } else if (name.startsWith("astrbot_") || name.startsWith("astrbot-")) { |
| plugin.trimmedName = name.substring(8); |
| } else plugin.trimmedName = plugin.name; |
| } |
| }); |
| }; |
| |
| const checkAlreadyInstalled = () => { |
| const data = Array.isArray(extension_data?.data) ? extension_data.data : []; |
| const installedRepos = new Set(data.map((ext) => ext.repo?.toLowerCase())); |
| const installedNames = new Set(data.map((ext) => ext.name)); |
| const installedByRepo = new Map( |
| data |
| .filter((ext) => ext.repo) |
| .map((ext) => [ext.repo.toLowerCase(), ext]), |
| ); |
| const installedByName = new Map(data.map((ext) => [ext.name, ext])); |
| |
| for (let i = 0; i < pluginMarketData.value.length; i++) { |
| const plugin = pluginMarketData.value[i]; |
| const matchedInstalled = |
| (plugin.repo && installedByRepo.get(plugin.repo.toLowerCase())) || |
| installedByName.get(plugin.name); |
| |
| |
| if (matchedInstalled) { |
| if ( |
| (!Array.isArray(plugin.support_platforms) || |
| plugin.support_platforms.length === 0) && |
| Array.isArray(matchedInstalled.support_platforms) |
| ) { |
| plugin.support_platforms = matchedInstalled.support_platforms; |
| } |
| if (!plugin.astrbot_version && matchedInstalled.astrbot_version) { |
| plugin.astrbot_version = matchedInstalled.astrbot_version; |
| } |
| } |
| |
| plugin.installed = |
| installedRepos.has(plugin.repo?.toLowerCase()) || |
| installedNames.has(plugin.name); |
| } |
| |
| let installed = []; |
| let notInstalled = []; |
| for (let i = 0; i < pluginMarketData.value.length; i++) { |
| if (pluginMarketData.value[i].installed) { |
| installed.push(pluginMarketData.value[i]); |
| } else { |
| notInstalled.push(pluginMarketData.value[i]); |
| } |
| } |
| pluginMarketData.value = notInstalled.concat(installed); |
| }; |
| |
| const showVersionCompatibilityWarning = (message) => { |
| versionCompatibilityDialog.message = message; |
| versionCompatibilityDialog.show = true; |
| }; |
|
|
| const refreshExtensionsAfterInstallFailure = async () => { |
| try { |
| await getExtensions(); |
| } catch (error) { |
| console.debug("Failed to refresh extensions after install failure:", error); |
| } |
| }; |
| |
| const continueInstallIgnoringVersionWarning = async () => { |
| versionCompatibilityDialog.show = false; |
| await newExtension(true); |
| }; |
| |
| const cancelInstallOnVersionWarning = () => { |
| versionCompatibilityDialog.show = false; |
| }; |
|
|
| const handleInstallResponse = async (resData, { toastStatus = false } = {}) => { |
| if ( |
| resData.status === "warning" && |
| resData.data?.warning_type === "astrbot_version_incompatible" |
| ) { |
| onLoadingDialogResult(2, resData.message, -1); |
| showVersionCompatibilityWarning(resData.message); |
| await refreshExtensionsAfterInstallFailure(); |
| return false; |
| } |
|
|
| if (toastStatus) { |
| toast(resData.message, resData.status === "ok" ? "success" : "error"); |
| } |
|
|
| if (resData.status === "error") { |
| onLoadingDialogResult(2, resData.message, -1); |
| await refreshExtensionsAfterInstallFailure(); |
| return false; |
| } |
|
|
| return true; |
| }; |
|
|
| const performInstallRequest = async ({ source, ignoreVersionCheck }) => { |
| if (source === "file") { |
| const formData = new FormData(); |
| formData.append("file", upload_file.value); |
| formData.append("ignore_version_check", String(ignoreVersionCheck)); |
| return axios.post("/api/plugin/install-upload", formData, { |
| headers: { |
| "Content-Type": "multipart/form-data", |
| }, |
| }); |
| } |
|
|
| return axios.post("/api/plugin/install", { |
| url: extension_url.value, |
| proxy: getSelectedGitHubProxy(), |
| ignore_version_check: ignoreVersionCheck, |
| }); |
| }; |
|
|
| const finalizeSuccessfulInstall = async (resData, source) => { |
| if (source === "file") { |
| upload_file.value = null; |
| } else { |
| extension_url.value = ""; |
| } |
|
|
| onLoadingDialogResult(1, resData.message); |
| dialog.value = false; |
| await getExtensions(); |
| checkAlreadyInstalled(); |
|
|
| viewReadme({ |
| name: resData.data.name, |
| repo: resData.data.repo || null, |
| }); |
|
|
| await checkAndPromptConflicts(); |
| }; |
| |
| const newExtension = async (ignoreVersionCheck = false) => { |
| if (extension_url.value === "" && upload_file.value === null) { |
| toast(tm("messages.fillUrlOrFile"), "error"); |
| return; |
| } |
| |
| if (extension_url.value !== "" && upload_file.value !== null) { |
| toast(tm("messages.dontFillBoth"), "error"); |
| return; |
| } |
| loading_.value = true; |
| loadingDialog.title = tm("status.loading"); |
| loadingDialog.show = true; |
|
|
| const source = upload_file.value !== null ? "file" : "url"; |
| toast( |
| source === "file" |
| ? tm("messages.installing") |
| : tm("messages.installingFromUrl") + " " + extension_url.value, |
| "primary", |
| ); |
|
|
| try { |
| const res = await performInstallRequest({ source, ignoreVersionCheck }); |
| loading_.value = false; |
|
|
| const canContinue = await handleInstallResponse(res.data, { |
| toastStatus: source === "url", |
| }); |
| if (!canContinue) return; |
|
|
| await finalizeSuccessfulInstall(res.data, source); |
| } catch (err) { |
| loading_.value = false; |
| const message = resolveErrorMessage(err, tm("messages.installFailed")); |
| if (source === "url") { |
| toast(message, "error"); |
| } |
| onLoadingDialogResult(2, message, -1); |
| await refreshExtensionsAfterInstallFailure(); |
| } |
| }; |
| |
| const normalizePlatformList = (platforms) => { |
| if (!Array.isArray(platforms)) return []; |
| return platforms.filter((item) => typeof item === "string"); |
| }; |
| |
| const getPlatformDisplayList = (platforms) => { |
| return normalizePlatformList(platforms).map((platformId) => |
| getPlatformDisplayName(platformId), |
| ); |
| }; |
| |
| const resolveSelectedInstallPlugin = () => { |
| if ( |
| selectedMarketInstallPlugin.value && |
| selectedMarketInstallPlugin.value.repo === extension_url.value |
| ) { |
| return selectedMarketInstallPlugin.value; |
| } |
| return pluginMarketData.value.find((plugin) => plugin.repo === extension_url.value) || null; |
| }; |
| |
| const selectedInstallPlugin = computed(() => resolveSelectedInstallPlugin()); |
| |
| const checkInstallCompatibility = async () => { |
| installCompat.checked = false; |
| installCompat.compatible = true; |
| installCompat.message = ""; |
| |
| const plugin = selectedInstallPlugin.value; |
| if (!plugin?.astrbot_version || uploadTab.value !== "url") { |
| return; |
| } |
| |
| try { |
| const res = await axios.post("/api/plugin/check-compat", { |
| astrbot_version: plugin.astrbot_version, |
| }); |
| if (res.data.status === "ok") { |
| installCompat.checked = true; |
| installCompat.compatible = !!res.data.data?.compatible; |
| installCompat.message = res.data.data?.message || ""; |
| } |
| } catch (err) { |
| console.debug("Failed to check plugin compatibility:", err); |
| } |
| }; |
| |
| |
| const refreshPluginMarket = async () => { |
| refreshingMarket.value = true; |
| try { |
| |
| const data = await commonStore.getPluginCollections( |
| true, |
| selectedSource.value, |
| ); |
| pluginMarketData.value = data; |
| trimExtensionName(); |
| checkAlreadyInstalled(); |
| checkUpdate(); |
| refreshRandomPlugins(); |
| currentPage.value = 1; |
| |
| toast(tm("messages.refreshSuccess"), "success"); |
| } catch (err) { |
| toast(tm("messages.refreshFailed") + " " + err, "error"); |
| } finally { |
| refreshingMarket.value = false; |
| } |
| }; |
| |
| |
| onMounted(async () => { |
| if (!syncTabFromHash(getLocationHash())) { |
| await replaceTabRoute(router, route, activeTab.value); |
| } |
| await getExtensions(); |
| |
| |
| loadCustomSources(); |
| |
| |
| const plugin_name = Array.isArray(route.query.open_config) |
| ? route.query.open_config[0] |
| : route.query.open_config; |
| if (plugin_name) { |
| console.log(`Opening config for plugin: ${plugin_name}`); |
| openExtensionConfig(plugin_name); |
| } |
| |
| try { |
| const data = await commonStore.getPluginCollections( |
| false, |
| selectedSource.value, |
| ); |
| pluginMarketData.value = data; |
| trimExtensionName(); |
| checkAlreadyInstalled(); |
| checkUpdate(); |
| refreshRandomPlugins(); |
| } catch (err) { |
| toast(tm("messages.getMarketDataFailed") + " " + err, "error"); |
| } |
| }); |
| |
| |
| const handleLocaleChange = () => { |
| |
| if (configDialog.value && currentConfigPlugin.value) { |
| openExtensionConfig(currentConfigPlugin.value); |
| } |
| }; |
| |
| |
| window.addEventListener("astrbot-locale-changed", handleLocaleChange); |
| |
| |
| onUnmounted(() => { |
| window.removeEventListener("astrbot-locale-changed", handleLocaleChange); |
| }); |
| |
| |
| let searchDebounceTimer = null; |
| watch(marketSearch, (newVal) => { |
| if (searchDebounceTimer) { |
| clearTimeout(searchDebounceTimer); |
| } |
| |
| searchDebounceTimer = setTimeout(() => { |
| debouncedMarketSearch.value = newVal; |
| |
| currentPage.value = 1; |
| }, 300); |
| }); |
| |
| |
| watch(isListView, (newVal) => { |
| if (typeof window !== "undefined" && window.localStorage) { |
| localStorage.setItem("pluginListViewMode", String(newVal)); |
| } |
| }); |
| |
| watch( |
| [() => dialog.value, () => extension_url.value, () => uploadTab.value], |
| async ([dialogOpen, _, currentUploadTab]) => { |
| if (!dialogOpen || currentUploadTab !== "url") { |
| installCompat.checked = false; |
| installCompat.compatible = true; |
| installCompat.message = ""; |
| return; |
| } |
| await checkInstallCompatibility(); |
| }, |
| ); |
| |
| watch( |
| () => route.hash, |
| (newHash) => { |
| const tab = extractTabFromHash(newHash); |
| if (tab && tab !== activeTab.value) { |
| activeTab.value = tab; |
| } |
| }, |
| ); |
| |
| watch(activeTab, (newTab) => { |
| if (!isValidTab(newTab)) return; |
| if (route.hash === `#${newTab}`) return; |
| void replaceTabRoute(router, route, newTab); |
| }); |
|
|
| return { |
| commonStore, |
| t, |
| tm, |
| router, |
| route, |
| getSelectedGitHubProxy, |
| conflictDialog, |
| checkAndPromptConflicts, |
| handleConflictConfirm, |
| fileInput, |
| activeTab, |
| validTabs, |
| isValidTab, |
| getLocationHash, |
| extractTabFromHash, |
| syncTabFromHash, |
| extension_data, |
| getInitialShowReserved, |
| showReserved, |
| snack_message, |
| snack_show, |
| snack_success, |
| configDialog, |
| extension_config, |
| pluginMarketData, |
| loadingDialog, |
| showPluginInfoDialog, |
| selectedPlugin, |
| curr_namespace, |
| updatingAll, |
| readmeDialog, |
| forceUpdateDialog, |
| updateAllConfirmDialog, |
| changelogDialog, |
| getInitialListViewMode, |
| isListView, |
| pluginSearch, |
| installedStatusFilter, |
| installedSortBy, |
| installedSortOrder, |
| loading_, |
| currentPage, |
| dangerConfirmDialog, |
| selectedDangerPlugin, |
| selectedMarketInstallPlugin, |
| installCompat, |
| versionCompatibilityDialog, |
| showUninstallDialog, |
| uninstallTarget, |
| showSourceDialog, |
| showSourceManagerDialog, |
| sourceName, |
| sourceUrl, |
| customSources, |
| selectedSource, |
| showRemoveSourceDialog, |
| sourceToRemove, |
| editingSource, |
| originalSourceUrl, |
| extension_url, |
| dialog, |
| upload_file, |
| uploadTab, |
| showPluginFullName, |
| marketSearch, |
| debouncedMarketSearch, |
| refreshingMarket, |
| sortBy, |
| sortOrder, |
| randomPluginNames, |
| showRandomPlugins, |
| normalizeStr, |
| toPinyinText, |
| toInitials, |
| plugin_handler_info_headers, |
| installedSortItems, |
| installedSortUsesOrder, |
| pluginHeaders, |
| filteredExtensions, |
| filteredPlugins, |
| filteredMarketPlugins, |
| sortedPlugins, |
| RANDOM_PLUGINS_COUNT, |
| randomPlugins, |
| shufflePlugins, |
| refreshRandomPlugins, |
| toggleRandomPluginsVisibility, |
| collapseRandomPlugins, |
| displayItemsPerPage, |
| totalPages, |
| paginatedPlugins, |
| updatableExtensions, |
| toggleShowReserved, |
| toast, |
| resetLoadingDialog, |
| onLoadingDialogResult, |
| failedPluginsDict, |
| failedPluginItems, |
| getExtensions, |
| handleReloadAllFailed, |
| reloadFailedPlugin, |
| checkUpdate, |
| uninstallExtension, |
| requestUninstallPlugin, |
| requestUninstallFailedPlugin, |
| handleUninstallConfirm, |
| updateExtension, |
| showUpdateAllConfirm, |
| confirmUpdateAll, |
| cancelUpdateAll, |
| confirmForceUpdate, |
| updateAllExtensions, |
| pluginOn, |
| pluginOff, |
| openExtensionConfig, |
| updateConfig, |
| showPluginInfo, |
| reloadPlugin, |
| viewReadme, |
| viewChangelog, |
| handleInstallPlugin, |
| confirmDangerInstall, |
| cancelDangerInstall, |
| loadCustomSources, |
| saveCustomSources, |
| addCustomSource, |
| openSourceManagerDialog, |
| selectPluginSource, |
| sourceSelectItems, |
| editCustomSource, |
| removeCustomSource, |
| confirmRemoveSource, |
| saveCustomSource, |
| trimExtensionName, |
| checkAlreadyInstalled, |
| showVersionCompatibilityWarning, |
| continueInstallIgnoringVersionWarning, |
| cancelInstallOnVersionWarning, |
| newExtension, |
| normalizePlatformList, |
| getPlatformDisplayList, |
| resolveSelectedInstallPlugin, |
| selectedInstallPlugin, |
| checkInstallCompatibility, |
| refreshPluginMarket, |
| handleLocaleChange, |
| searchDebounceTimer, |
| }; |
| }; |
|
|