mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-14 03:33:43 +08:00
411 lines
12 KiB
Vue
411 lines
12 KiB
Vue
<template>
|
|
<div class="mc-page-shell">
|
|
<div class="mc-page-frame">
|
|
<div class="mc-page-inner plugins-page">
|
|
<div class="mc-page-header">
|
|
<div>
|
|
<div class="mc-page-kicker">Extension</div>
|
|
<h1 class="mc-page-title">{{ t('plugins.title') }}</h1>
|
|
<p class="mc-page-desc">{{ t('plugins.desc') }}</p>
|
|
</div>
|
|
<button class="btn-secondary" @click="refresh">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="23 4 23 10 17 10"/>
|
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
|
</svg>
|
|
{{ t('plugins.refresh') }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Loading -->
|
|
<div v-if="loading" class="loading-state mc-surface-card">
|
|
<div class="loading-spinner"></div>
|
|
<p>{{ t('plugins.loading') }}</p>
|
|
</div>
|
|
|
|
<!-- Plugin Cards -->
|
|
<div v-else class="plugins-grid">
|
|
<div v-for="plugin in plugins" :key="plugin.name" class="plugin-card mc-surface-card">
|
|
<div class="plugin-header">
|
|
<div class="plugin-icon-wrap">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>
|
|
<path d="M16 3h-8v4h8V3z"/>
|
|
</svg>
|
|
</div>
|
|
<div class="plugin-meta">
|
|
<div class="plugin-name">{{ plugin.displayName || plugin.name }}</div>
|
|
<div class="plugin-version">v{{ plugin.version }}</div>
|
|
</div>
|
|
<label class="toggle-switch" :class="{ disabled: toggling === plugin.name }">
|
|
<input type="checkbox" :checked="plugin.enabled"
|
|
:disabled="toggling === plugin.name"
|
|
@change="togglePlugin(plugin)" />
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
</div>
|
|
|
|
<p class="plugin-desc">{{ plugin.description || t('plugins.noDescription') }}</p>
|
|
|
|
<div class="plugin-details">
|
|
<div class="plugin-detail-row">
|
|
<span class="detail-label">{{ t('plugins.type') }}</span>
|
|
<span class="type-badge" :class="'type-' + plugin.type">{{ plugin.type }}</span>
|
|
</div>
|
|
<div class="plugin-detail-row" v-if="plugin.author">
|
|
<span class="detail-label">{{ t('plugins.author') }}</span>
|
|
<span class="detail-value">{{ plugin.author }}</span>
|
|
</div>
|
|
<div class="plugin-detail-row">
|
|
<span class="detail-label">{{ t('plugins.status') }}</span>
|
|
<span class="status-badge" :class="'status-' + (plugin.status || '').toLowerCase()">
|
|
{{ plugin.status }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Registered capabilities -->
|
|
<div class="plugin-capabilities" v-if="hasCapabilities(plugin)">
|
|
<div class="capability-section" v-if="plugin.registeredTools?.length">
|
|
<span class="capability-label">{{ t('plugins.tools') }}:</span>
|
|
<span class="capability-tag" v-for="tool in plugin.registeredTools" :key="tool">{{ tool }}</span>
|
|
</div>
|
|
<div class="capability-section" v-if="plugin.registeredChannels?.length">
|
|
<span class="capability-label">{{ t('plugins.channels') }}:</span>
|
|
<span class="capability-tag" v-for="ch in plugin.registeredChannels" :key="ch">{{ ch }}</span>
|
|
</div>
|
|
<div class="capability-section" v-if="plugin.registeredProvider">
|
|
<span class="capability-label">{{ t('plugins.provider') }}:</span>
|
|
<span class="capability-tag">{{ plugin.registeredProvider }}</span>
|
|
</div>
|
|
<div class="capability-section" v-if="plugin.registeredMemoryProvider">
|
|
<span class="capability-label">{{ t('plugins.memoryProvider') }}:</span>
|
|
<span class="capability-tag">{{ plugin.registeredMemoryProvider }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error message -->
|
|
<div class="plugin-error" v-if="plugin.errorMessage">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<line x1="15" y1="9" x2="9" y2="15"/>
|
|
<line x1="9" y1="9" x2="15" y2="15"/>
|
|
</svg>
|
|
{{ plugin.errorMessage }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div v-if="plugins.length === 0 && !loading" class="empty-state mc-surface-card">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="empty-icon">
|
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>
|
|
<path d="M16 3h-8v4h8V3z"/>
|
|
</svg>
|
|
<p class="empty-title">{{ t('plugins.emptyTitle') }}</p>
|
|
<p class="empty-hint">{{ t('plugins.emptyHint') }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { ElMessage } from 'element-plus'
|
|
import { pluginApi } from '@/api'
|
|
|
|
const { t } = useI18n()
|
|
|
|
interface PluginInfo {
|
|
name: string
|
|
version: string
|
|
type: string
|
|
displayName: string
|
|
description: string
|
|
author: string
|
|
enabled: boolean
|
|
status: string
|
|
errorMessage?: string
|
|
jarPath?: string
|
|
registeredTools?: string[]
|
|
registeredChannels?: string[]
|
|
registeredProvider?: string
|
|
registeredMemoryProvider?: string
|
|
}
|
|
|
|
const plugins = ref<PluginInfo[]>([])
|
|
const loading = ref(false)
|
|
const toggling = ref<string | null>(null)
|
|
|
|
async function loadPlugins() {
|
|
loading.value = true
|
|
try {
|
|
const res = await pluginApi.list()
|
|
plugins.value = res.data || []
|
|
} catch (e: any) {
|
|
ElMessage.error(t('plugins.loadFailed'))
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function togglePlugin(plugin: PluginInfo) {
|
|
toggling.value = plugin.name
|
|
try {
|
|
if (plugin.enabled) {
|
|
await pluginApi.disable(plugin.name)
|
|
ElMessage.success(t('plugins.disabled', { name: plugin.displayName || plugin.name }))
|
|
} else {
|
|
await pluginApi.enable(plugin.name)
|
|
ElMessage.success(t('plugins.enabled', { name: plugin.displayName || plugin.name }))
|
|
}
|
|
await loadPlugins()
|
|
} catch (e: any) {
|
|
ElMessage.error(e.message || t('plugins.toggleFailed'))
|
|
await loadPlugins()
|
|
} finally {
|
|
toggling.value = null
|
|
}
|
|
}
|
|
|
|
function refresh() {
|
|
loadPlugins()
|
|
}
|
|
|
|
function hasCapabilities(plugin: PluginInfo): boolean {
|
|
return !!(
|
|
plugin.registeredTools?.length ||
|
|
plugin.registeredChannels?.length ||
|
|
plugin.registeredProvider ||
|
|
plugin.registeredMemoryProvider
|
|
)
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadPlugins()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.plugins-page { gap: 18px; }
|
|
|
|
.plugins-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
|
gap: 16px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.plugin-card {
|
|
padding: 20px;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.plugin-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.plugin-icon-wrap {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--mc-accent-bg, #f0f0ff);
|
|
color: var(--mc-accent, #6366f1);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.plugin-meta {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.plugin-name {
|
|
font-weight: 600;
|
|
font-size: 15px;
|
|
color: var(--mc-text-primary);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.plugin-version {
|
|
font-size: 12px;
|
|
color: var(--mc-text-tertiary);
|
|
margin-top: 1px;
|
|
}
|
|
|
|
.plugin-desc {
|
|
font-size: 13px;
|
|
color: var(--mc-text-secondary);
|
|
line-height: 1.5;
|
|
margin: 0;
|
|
}
|
|
|
|
.plugin-details {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.plugin-detail-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.detail-label {
|
|
color: var(--mc-text-tertiary);
|
|
min-width: 60px;
|
|
}
|
|
|
|
.detail-value {
|
|
color: var(--mc-text-secondary);
|
|
}
|
|
|
|
.type-badge {
|
|
font-size: 11px;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
}
|
|
.type-tool { background: #dbeafe; color: #1d4ed8; }
|
|
.type-provider { background: #fef3c7; color: #92400e; }
|
|
.type-channel { background: #d1fae5; color: #065f46; }
|
|
.type-memory { background: #ede9fe; color: #5b21b6; }
|
|
|
|
.status-badge {
|
|
font-size: 11px;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-weight: 500;
|
|
}
|
|
.status-enabled { background: #d1fae5; color: #065f46; }
|
|
.status-disabled { background: #f3f4f6; color: #6b7280; }
|
|
.status-error { background: #fee2e2; color: #991b1b; }
|
|
.status-loaded { background: #dbeafe; color: #1d4ed8; }
|
|
|
|
.plugin-capabilities {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
padding-top: 4px;
|
|
border-top: 1px solid var(--mc-border);
|
|
}
|
|
|
|
.capability-section {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.capability-label {
|
|
color: var(--mc-text-tertiary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.capability-tag {
|
|
background: var(--mc-surface-hover, #f5f5f5);
|
|
color: var(--mc-text-secondary);
|
|
padding: 1px 6px;
|
|
border-radius: 3px;
|
|
font-family: var(--mc-font-mono, monospace);
|
|
font-size: 11px;
|
|
}
|
|
|
|
.plugin-error {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 6px;
|
|
padding: 8px 10px;
|
|
background: #fef2f2;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
color: #991b1b;
|
|
line-height: 1.4;
|
|
}
|
|
.plugin-error svg { flex-shrink: 0; margin-top: 1px; }
|
|
|
|
/* Toggle switch (reuse pattern from Tools.vue) */
|
|
.toggle-switch { position: relative; display: inline-block; width: 36px; height: 20px; flex-shrink: 0; }
|
|
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
|
.toggle-slider {
|
|
position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
|
|
background: var(--mc-border, #d1d5db); border-radius: 20px; transition: 0.2s;
|
|
}
|
|
.toggle-slider:before {
|
|
position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px;
|
|
background: white; border-radius: 50%; transition: 0.2s;
|
|
}
|
|
.toggle-switch input:checked + .toggle-slider { background: var(--mc-accent, #6366f1); }
|
|
.toggle-switch input:checked + .toggle-slider:before { transform: translateX(16px); }
|
|
.toggle-switch.disabled { opacity: 0.5; pointer-events: none; }
|
|
|
|
/* Loading state */
|
|
.loading-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 48px 24px;
|
|
text-align: center;
|
|
border-radius: 12px;
|
|
color: var(--mc-text-tertiary);
|
|
}
|
|
.loading-spinner {
|
|
width: 28px; height: 28px;
|
|
border: 3px solid var(--mc-border, #e5e7eb);
|
|
border-top-color: var(--mc-accent, #6366f1);
|
|
border-radius: 50%;
|
|
animation: spin 0.7s linear infinite;
|
|
margin-bottom: 12px;
|
|
}
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
/* Empty state */
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 48px 24px;
|
|
text-align: center;
|
|
border-radius: 12px;
|
|
}
|
|
.empty-icon { color: var(--mc-text-tertiary); margin-bottom: 12px; }
|
|
.empty-title { font-weight: 600; color: var(--mc-text-primary); margin: 0 0 4px; }
|
|
.empty-hint { font-size: 13px; color: var(--mc-text-tertiary); margin: 0; }
|
|
|
|
/* btn-secondary reuse */
|
|
.btn-secondary {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
padding: 8px 16px; border-radius: 8px; font-size: 13px; font-weight: 500;
|
|
background: var(--mc-surface-hover, #f5f5f5); color: var(--mc-text-primary);
|
|
border: 1px solid var(--mc-border, #e5e7eb); cursor: pointer; transition: 0.15s;
|
|
}
|
|
.btn-secondary:hover { background: var(--mc-surface-active, #ebebeb); }
|
|
|
|
/* Dark mode overrides */
|
|
:root.dark .type-tool { background: #1e3a5f; color: #93c5fd; }
|
|
:root.dark .type-provider { background: #451a03; color: #fcd34d; }
|
|
:root.dark .type-channel { background: #064e3b; color: #6ee7b7; }
|
|
:root.dark .type-memory { background: #2e1065; color: #c4b5fd; }
|
|
:root.dark .status-enabled { background: #064e3b; color: #6ee7b7; }
|
|
:root.dark .status-disabled { background: #374151; color: #9ca3af; }
|
|
:root.dark .status-error { background: #450a0a; color: #fca5a5; }
|
|
:root.dark .status-loaded { background: #1e3a5f; color: #93c5fd; }
|
|
:root.dark .plugin-error { background: #450a0a; color: #fca5a5; }
|
|
:root.dark .plugin-icon-wrap { background: #2e1065; }
|
|
</style>
|