feat(settings): add update check and MCP uninstall

This commit is contained in:
大熊 authored and GitHub committed 2026-08-15 14:44:46 +08:00
1 parent 745483cd6c
commit 2ea5e8e9b9
12 files changed
+189 -9

No files matched your search

@@ -67,6 +67,7 @@ import {
aiTestConnection,
checkMcpServerStatus,
installMcpServer,
uninstallMcpServer,
forgetSnippetSavedToken,
forgetWebdavSyncSecretsPassphrase,
forgetWebdavSavedPassword,
@@ -196,10 +197,12 @@ const props = defineProps<{
initialSection?: string;
navigationRequestId?: number;
appVersion?: string;
checkingUpdates?: boolean;
}>();
const emit = defineEmits<{
"update:open": [value: boolean];
"check-updates": [];
}>();
const isSettingsPage = computed(() => props.variant === "page");
@@ -1723,7 +1726,7 @@ async function exportDebugLogs() {
// ---------- MCP Server ----------
type McpConfigTab = "claude" | "cursor" | "trae" | "vscode" | "windsurf" | "codex" | "opencode" | "cherry-studio";
type McpCopyKind = "install" | `${McpConfigTab}-config`;
type McpCopyKind = "install" | "uninstall" | `${McpConfigTab}-config`;
const mcpStatus = ref<McpServerStatus | null>(null);
const mcpStatusLoading = ref(false);
@@ -1736,6 +1739,7 @@ const mcpPolicyLoading = ref(false);
const mcpPolicySaving = ref(false);
const mcpPolicyLoadError = ref("");
const mcpInstalling = ref(false);
const mcpUninstalling = ref(false);
const mcpInstallMessage = ref("");
const mcpInstallError = ref(false);
const mcpExecutionMode = computed(() => mcpExecutionModeFromPolicy(settingsStore.mcpGlobalPolicy));
@@ -1854,6 +1858,8 @@ const mcpCommand = computed(() => {
return mcpStatus.value.installed ? mcpStatus.value.update_command : mcpStatus.value.install_command;
});
const mcpUninstallCommand = computed(() => mcpStatus.value?.uninstall_command || "npm uninstall -g @dbx-app/mcp-server");
async function refreshMcpStatus() {
if (mcpStatusLoading.value) return;
mcpStatusLoading.value = true;
@@ -1888,7 +1894,7 @@ async function copyMcpText(kind: McpCopyKind, value: string) {
}
async function installMcp() {
if (mcpInstalling.value) return;
if (mcpInstalling.value || mcpUninstalling.value) return;
mcpInstalling.value = true;
mcpInstallMessage.value = "";
mcpInstallError.value = false;
@@ -1911,6 +1917,27 @@ async function installMcp() {
}
}
async function uninstallMcp() {
if (mcpInstalling.value || mcpUninstalling.value || !mcpStatus.value?.installed) return;
if (!window.confirm(t("settings.mcpUninstallConfirm"))) return;
mcpUninstalling.value = true;
mcpInstallMessage.value = "";
mcpInstallError.value = false;
try {
mcpInstallMessage.value = await uninstallMcpServer();
await refreshMcpStatus();
} catch (e: any) {
mcpInstallMessage.value = t("settings.mcpUninstallFailed", { error: e?.message || String(e) });
mcpInstallError.value = true;
} finally {
mcpUninstalling.value = false;
window.setTimeout(() => {
mcpInstallMessage.value = "";
mcpInstallError.value = false;
}, 3000);
}
}
// ---------- WebDAV Sync ----------
const webdavEndpoint = ref(localStorage.getItem("dbx-webdav-endpoint") || "");
const webdavUsername = ref(localStorage.getItem("dbx-webdav-username") || "");
@@ -2873,7 +2900,7 @@ const aiSupportsAnthropicApiStyle = computed(() => aiEditProvider.value === "cus
const aiCliMcpNeedsInstall = computed(() => aiIsCliProvider.value && (!mcpStatus.value || !mcpStatus.value.installed));
const aiCliMcpCanInstall = computed(() => {
const status = mcpStatus.value;
return !mcpInstalling.value && !!status?.npm_available && (!status.installed || status.update_available);
return !mcpInstalling.value && !mcpUninstalling.value && !!status?.npm_available && (!status.installed || status.update_available);
});
const aiCliMcpActionLabel = computed(() => {
if (!mcpStatus.value?.installed) return t("settings.mcpInstallButton");
@@ -6273,7 +6300,7 @@ onUnmounted(() => {
<CheckCircle2 v-if="mcpCopied === 'install'" class="h-4 w-4 text-green-500" />
<Copy v-else class="h-4 w-4" />
</Button>
<Button type="button" variant="default" :disabled="mcpInstalling || !mcpStatus?.npm_available || (mcpStatus?.installed && !mcpStatus?.update_available)" @click="installMcp">
<Button type="button" variant="default" :disabled="mcpInstalling || mcpUninstalling || !mcpStatus?.npm_available || (mcpStatus?.installed && !mcpStatus?.update_available)" @click="installMcp">
<Loader2 v-if="mcpInstalling" class="mr-2 h-4 w-4 animate-spin" />
<CheckCircle2 v-if="!mcpInstalling && mcpStatus?.installed && !mcpStatus?.update_available" class="mr-2 h-4 w-4" />
{{ mcpInstalling ? t("settings.mcpInstalling") : !mcpStatus?.installed ? t("settings.mcpInstallButton") : mcpStatus?.update_available ? t("settings.mcpUpdateButton") : t("settings.mcpUpToDate") }}
@@ -6287,6 +6314,24 @@ onUnmounted(() => {
</div>
</div>
<div v-if="!isWeb && mcpStatus?.installed" class="space-y-2">
<Label>{{ t("settings.mcpUninstallCommand") }}</Label>
<div class="flex min-w-0 items-center gap-2">
<div class="min-w-0 flex-1 overflow-x-auto rounded-md border bg-background px-3 py-2 font-mono text-xs whitespace-nowrap">
{{ mcpUninstallCommand }}
</div>
<Button type="button" variant="outline" size="icon" :title="t('common.copy')" @click="copyMcpText('uninstall', mcpUninstallCommand)">
<CheckCircle2 v-if="mcpCopied === 'uninstall'" class="h-4 w-4 text-green-500" />
<Copy v-else class="h-4 w-4" />
</Button>
<Button type="button" variant="outline" class="text-destructive hover:text-destructive" :disabled="mcpInstalling || mcpUninstalling || !mcpStatus.npm_available" @click="uninstallMcp">
<Loader2 v-if="mcpUninstalling" class="mr-2 h-4 w-4 animate-spin" />
<Trash2 v-else class="mr-2 h-4 w-4" />
{{ mcpUninstalling ? t("settings.mcpUninstalling") : t("settings.mcpUninstallButton") }}
</Button>
</div>
</div>
<div class="space-y-2">
<p class="text-xs text-muted-foreground">
{{ t("settings.mcpConfigOptionsHint") }}
@@ -6611,7 +6656,7 @@ onUnmounted(() => {
</p>
</div>
<ChangelogPanel />
<ChangelogPanel :checking-updates="props.checkingUpdates" @check-updates="emit('check-updates')" />
<div class="rounded-lg border p-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { ChevronDown, ExternalLink, Loader2, RefreshCw, Tag } from "@lucide/vue";
import { ChevronDown, CloudDownload, ExternalLink, Loader2, RefreshCw, Tag } from "@lucide/vue";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
@@ -13,6 +13,19 @@ const PAGE_SIZE = 5;
const { t, locale } = useI18n();
withDefaults(
defineProps<{
checkingUpdates?: boolean;
}>(),
{
checkingUpdates: false,
},
);
const emit = defineEmits<{
"check-updates": [];
}>();
const loading = ref(false);
const error = ref("");
const releases = ref<ChangelogRelease[]>([]);
@@ -123,6 +136,11 @@ watch(changelogLang, (lang) => {
<p class="text-sm text-muted-foreground">{{ t("settings.changelogDescription") }}</p>
</div>
<div class="settings-about-section-actions flex shrink-0 flex-wrap items-center gap-2">
<Button type="button" variant="outline" size="sm" :disabled="checkingUpdates" @click="emit('check-updates')">
<Loader2 v-if="checkingUpdates" class="mr-1 h-3.5 w-3.5 animate-spin" />
<CloudDownload v-else class="mr-1 h-3.5 w-3.5" />
{{ t("updates.check") }}
</Button>
<Button type="button" variant="outline" size="sm" :disabled="loading" @click="openExternalUrl(changelogWebsiteUrl(changelogLang))">
<ExternalLink class="mr-1 h-3.5 w-3.5" />
{{ t("settings.changelogOpenWebsite") }}