feat(extensions): add ZIP installation and unify action dialogs

This commit is contained in:
2026-09-06 00:52:59 +08:00
parent e3d522b412
commit aa88f23e9c
29 changed files with 554 additions and 50 deletions
@@ -1,4 +1,7 @@
<script setup lang="ts">
import ActionDialog from '@/components/common/ActionDialog.vue'
import { useActionDialog } from '@/composables/useActionDialog'
const { actionDialog, resolveAction, askConfirm } = useActionDialog()
import { Key, Refresh } from '@element-plus/icons-vue'
import { computed, ref, watch } from 'vue'
import AppIcon from '@/components/common/AppIcon.vue'
@@ -116,11 +119,14 @@ async function saveSecret(field: PluginSettingField) {
} catch (reason) { feedback(message(reason, t('密钥保存失败', 'Failed to save secret'))) } finally { busy.value = '' }
}
async function deleteSecret(field: PluginSettingField) {
if (!confirm(t('删除已保存的', 'Delete saved ') + field.label + '')) return
const pluginId = props.plugin.plugin_id
if (!(await askConfirm(t('删除已保存的', 'Delete saved ') + field.label + ''))) return
if (pluginId !== props.plugin.plugin_id) return
busy.value = 'secret:' + field.key
feedback()
try {
const state = await pluginService.deletePluginSecret(props.plugin.plugin_id, field.key)
const state = await pluginService.deletePluginSecret(pluginId, field.key)
if (pluginId !== props.plugin.plugin_id) return
if (schema.value) schema.value.secrets[field.key] = { configured: state.configured }
secrets.value[field.key] = ''
notice.value = field.label + t('已删除。', ' deleted.')
@@ -130,6 +136,7 @@ async function deleteSecret(field: PluginSettingField) {
<template>
<section class="mcp-panel">
<ActionDialog v-if="actionDialog" v-bind="actionDialog" @resolve="resolveAction" />
<nav class="mcp-tabs" :aria-label="t('MCP Plugin 配置', 'MCP and Plugin settings')">
<button v-for="tab in tabs" :key="tab.id" :class="{ active: activeTab === tab.id }" @click="selectTab(tab.id)">{{ tab.label }}</button>
</nav>
@@ -54,13 +54,17 @@ it.each(['save', 'delete'] as const)('ignores old secret %s responses after swit
let finish!: () => void
vi.mocked(service.putPluginSecret).mockReturnValue(new Promise(resolve => { finish = () => resolve({ plugin_id: 'demo', key: 'token', configured: true }) }))
if (action === 'delete') {
vi.stubGlobal('confirm', vi.fn(() => true))
vi.mocked(service.deletePluginSecret).mockReturnValue(new Promise(resolve => { finish = () => resolve({ plugin_id: 'demo', key: 'token', configured: false }) }))
}
wrapper = mount(PluginSettingsPanel, { props: { pluginId: 'demo' } })
await flushPromises()
await wrapper.get('input[type="password"]').setValue('old-fixture-value')
await wrapper.get(action === 'save' ? '.secret-row button' : '.secret-row .danger').trigger('click')
if (action === 'delete') {
await wrapper.get('.action-dialog').trigger('submit')
await flushPromises()
}
vi.mocked(service.getPluginSettings).mockResolvedValue(secretSchema(false))
await wrapper.setProps({ pluginId: 'other' })
await flushPromises()
@@ -1,4 +1,7 @@
<script setup lang="ts">
import ActionDialog from '@/components/common/ActionDialog.vue'
import { useActionDialog } from '@/composables/useActionDialog'
const { actionDialog, resolveAction, askConfirm } = useActionDialog()
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import type { PluginSettingsSchema, PluginSettingField } from '@/contracts'
import {
@@ -106,9 +109,10 @@ async function saveSecret(key: string) {
async function clearSecret(key: string) {
if (!schema.value || isSaving.value) return
if (!confirm(`确认删除 " ${key} " 的配置?`)) return
const version = loadVersion
const pluginId = props.pluginId
if (!(await askConfirm(`确认删除 " ${key} " 的配置?`))) return
if (version !== loadVersion || pluginId !== props.pluginId) return
isSaving.value = true
saveError.value = ''
try {
@@ -145,6 +149,7 @@ watch(() => props.pluginId, load)
<template>
<div class="plugin-settings-panel">
<ActionDialog v-if="actionDialog" v-bind="actionDialog" @resolve="resolveAction" />
<div v-if="isLoading" class="loading">加载设置中</div>
<template v-else-if="schema && schema.fields.length > 0">
@@ -1,4 +1,7 @@
<script setup lang="ts">
import ActionDialog from '@/components/common/ActionDialog.vue'
import { useActionDialog } from '@/composables/useActionDialog'
const { actionDialog, resolveAction, askConfirm } = useActionDialog()
import { Connection } from '@element-plus/icons-vue'
import AppIcon from '@/components/common/AppIcon.vue'
import ExtensionInstallDialog from '@/components/common/ExtensionInstallDialog.vue'
@@ -39,13 +42,13 @@ async function toggle(id: string, enabled: boolean) {
}
async function grant(id: string, permissions: string[]) {
if (!confirm(`${t('将授权:', 'Grant permissions: ')}${permissions.join(', ')}${t('是否继续?', 'Continue?')}`)) return
if (!(await askConfirm(`${t('将授权:', 'Grant permissions: ')}${permissions.join(', ')}${t('是否继续?', 'Continue?')}`))) return
try { await pluginStore.grantPermissions(id, permissions) }
catch (error) { actionError.value = error instanceof Error ? error.message : t('授权失败', 'Authorization failed') }
}
async function uninstall(id: string, name: string) {
if (!confirm(t(`卸载「${name}」将移除其全部 Contribution,是否继续?`, `Uninstalling “${name}” removes all its contributions. Continue?`))) return
if (!(await askConfirm(t(`卸载「${name}」将移除其全部 Contribution,是否继续?`, `Uninstalling “${name}” removes all its contributions. Continue?`)))) return
try { await pluginStore.uninstallPlugin(id) }
catch (error) { actionError.value = error instanceof Error ? error.message : t('卸载失败', 'Uninstall failed') }
}
@@ -61,6 +64,7 @@ const hasCommandContribution = computed(() =>
<template>
<section class="feature-page">
<ActionDialog v-if="actionDialog" v-bind="actionDialog" @resolve="resolveAction" />
<ExtensionInstallDialog v-if="showInstall" kind="Plugin" :install="pluginStore.installPlugin" @close="showInstall = false" @installed="showInstall = false; actionError = ''" />
<header class="feature-header">
<div><h1>{{ t('Plugin 与 MCP', 'Plugins and MCP') }}</h1><p>{{ t('管理插件生命周期、MCP Host、权限和受控 Contribution。', 'Manage plugin lifecycles, MCP hosts, permissions, and controlled contributions.') }}</p></div>