Fix/frontend review findings #3

Merged
Kronecker merged 16 commits from fix/frontend-review-findings into main 2026-08-30 00:16:35 +08:00
7 changed files with 189 additions and 29 deletions
Showing only changes of commit 3239bab696 - Show all commits
@@ -0,0 +1,36 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { usePluginStore } from '@/stores/plugin'
import { useSkillStore } from '@/stores/skill'
const route = useRoute()
const skillStore = useSkillStore()
const pluginStore = usePluginStore()
const isPlugin = computed(() => route.name === 'plugins')
onMounted(() => { if (isPlugin.value) void pluginStore.loadPlugins(); else void skillStore.loadSkills() })
</script>
<template>
<div class="sidebar-panel">
<div v-if="isPlugin" class="sidebar-list">
<button v-for="plugin in pluginStore.plugins" :key="plugin.plugin_id" class="sidebar-list-item extension-item"
:class="{ active: pluginStore.selectedPluginId === plugin.plugin_id }" @click="pluginStore.selectPlugin(plugin.plugin_id)">
<span>{{ plugin.icon || '🧩' }}</span><span><strong>{{ plugin.name }}</strong><small>{{ plugin.status }}</small></span>
</button>
</div>
<div v-else class="sidebar-list">
<button v-for="skill in skillStore.skills" :key="skill.skill_id" class="sidebar-list-item extension-item"
:class="{ active: skillStore.selectedSkillId === skill.skill_id }" @click="skillStore.selectSkill(skill.skill_id)">
<span>{{ skill.icon || '⚡' }}</span><span><strong>{{ skill.name }}</strong><small>{{ skill.status }}</small></span>
</button>
</div>
</div>
</template>
<style scoped>
.extension-item { display: grid; grid-template-columns: auto 1fr; align-items: center; gap: var(--space-sm); width: 100%; text-align: left; }
.extension-item strong, .extension-item small { display: block; }
.extension-item small { color: var(--color-text-tertiary); }
</style>
+1
View File
@@ -246,6 +246,7 @@ export interface Plugin {
status: PluginStatus status: PluginStatus
enabled: boolean enabled: boolean
permissions: string[] permissions: string[]
granted_permissions?: string[]
contributions: PluginContribution[] contributions: PluginContribution[]
backend_type?: 'mcp' | 'internal_rpc' | 'none' backend_type?: 'mcp' | 'internal_rpc' | 'none'
transport?: 'stdio' | 'http' | 'none' transport?: 'stdio' | 'http' | 'none'
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { usePluginStore } from '@/stores/plugin'
const pluginStore = usePluginStore()
const actionError = ref('')
onMounted(() => { void pluginStore.loadPlugins() })
async function install() { const path = prompt('请输入 Plugin Package 路径')?.trim(); if (!path) return; try { await pluginStore.installPlugin(path) } catch (error) { actionError.value = error instanceof Error ? error.message : '安装失败' } }
async function toggle(id: string, enabled: boolean) { try { enabled ? await pluginStore.disablePlugin(id) : await pluginStore.enablePlugin(id) } catch (error) { actionError.value = error instanceof Error ? error.message : '状态更新失败' } }
async function grant(id: string, permissions: string[]) { if (!confirm(`将授权:${permissions.join('、')}。是否继续?`)) return; try { await pluginStore.grantPermissions(id, permissions) } catch (error) { actionError.value = error instanceof Error ? error.message : '授权失败' } }
async function uninstall(id: string, name: string) { if (!confirm(`卸载“${name}”将移除其全部 Contribution,是否继续?`)) return; try { await pluginStore.uninstallPlugin(id) } catch (error) { actionError.value = error instanceof Error ? error.message : '卸载失败' } }
</script>
<template>
<section class="feature-page">
<header class="feature-header"><div><h1>Plugin 管理</h1><p>管理插件生命周期权限和受控 Contribution</p></div><button class="button-primary" @click="install">安装 Plugin</button></header>
<div v-if="pluginStore.error || actionError" class="error-banner">{{ pluginStore.error || actionError }}</div>
<div v-if="pluginStore.selectedPlugin" class="panel">
<div class="detail-head"><div><span class="badge" :class="{ success: pluginStore.selectedPlugin.status === 'ready', error: pluginStore.selectedPlugin.status === 'error', warning: pluginStore.selectedPlugin.status === 'permission_required' }">{{ pluginStore.selectedPlugin.status }}</span><h2>{{ pluginStore.selectedPlugin.icon }} {{ pluginStore.selectedPlugin.name }}</h2><p class="muted">v{{ pluginStore.selectedPlugin.version }} · {{ pluginStore.selectedPlugin.backend_type || 'none' }}/{{ pluginStore.selectedPlugin.transport || 'none' }}</p></div><div class="inline-actions"><button v-if="pluginStore.selectedPlugin.status === 'permission_required'" class="button-primary" @click="grant(pluginStore.selectedPlugin.plugin_id, pluginStore.selectedPlugin.permissions)">授权权限</button><button class="button-secondary" @click="toggle(pluginStore.selectedPlugin.plugin_id, pluginStore.selectedPlugin.enabled)">{{ pluginStore.selectedPlugin.enabled ? '停用' : '启用' }}</button><button class="button-danger" @click="uninstall(pluginStore.selectedPlugin.plugin_id, pluginStore.selectedPlugin.name)">卸载</button></div></div>
<p class="description">{{ pluginStore.selectedPlugin.description }}</p>
<div class="detail-grid"><div><h3>权限</h3><div class="tag-list"><span v-for="permission in pluginStore.selectedPlugin.permissions" :key="permission" class="badge warning">{{ permission }}</span></div></div><div><h3>Contribution</h3><div class="contribution-list"><div v-for="item in pluginStore.selectedPlugin.contributions" :key="item.id" class="item-card"><span class="badge info">{{ item.type }}</span><strong>{{ item.name }}</strong><p class="subtle">{{ item.description || item.id }}</p></div></div></div></div>
<div v-if="pluginStore.selectedPlugin.last_error" class="error-banner last-error">{{ pluginStore.selectedPlugin.last_error }}</div>
<div v-if="pluginStore.selectedPlugin.dependent_skills?.length" class="notice-banner last-error">依赖此插件的 Skill{{ pluginStore.selectedPlugin.dependent_skills.join('') }}</div>
</div>
<div v-else class="feature-grid"><article v-for="plugin in pluginStore.plugins" :key="plugin.plugin_id" class="item-card extension-card" @click="pluginStore.selectPlugin(plugin.plugin_id)"><div class="extension-title"><span class="icon">{{ plugin.icon || '🧩' }}</span><div><strong>{{ plugin.name }}</strong><p>v{{ plugin.version }}</p></div><span class="badge" :class="{ success: plugin.status === 'ready', error: plugin.status === 'error', warning: plugin.status === 'permission_required' }">{{ plugin.status }}</span></div><p class="muted">{{ plugin.description }}</p><p class="subtle">{{ plugin.permissions.length }} 项权限 · {{ plugin.contributions.length }} Contribution</p></article></div>
</section>
</template>
<style scoped>
.detail-head, .extension-title { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-md); }
.detail-head h2 { margin-top: var(--space-sm); }
.description { margin: var(--space-xl) 0; line-height: var(--line-height-relaxed); }
.detail-grid { display: grid; grid-template-columns: minmax(220px, .7fr) minmax(320px, 1.3fr); gap: var(--space-xl); }
.detail-grid h3 { margin-bottom: var(--space-sm); }
.contribution-list { display: grid; gap: var(--space-sm); }
.contribution-list .item-card { display: grid; gap: var(--space-xs); }
.last-error { margin: var(--space-xl) 0 0; }
.extension-card { cursor: pointer; }
.extension-card > p { margin-top: var(--space-md); }
.extension-title { align-items: center; }
.extension-title .icon { font-size: 28px; }
.extension-title p { color: var(--color-text-tertiary); font-size: var(--font-size-xs); }
@media (max-width: 800px) { .detail-grid { grid-template-columns: 1fr; } }
</style>
@@ -0,0 +1,50 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useSkillStore } from '@/stores/skill'
const skillStore = useSkillStore()
const actionError = ref('')
onMounted(() => { void skillStore.loadSkills() })
async function install() {
const path = prompt('请输入 Skill Package 路径')?.trim()
if (!path) return
try { await skillStore.installSkill(path) } catch (error) { actionError.value = error instanceof Error ? error.message : '安装失败' }
}
async function toggle(skillId: string, enabled: boolean) {
try { enabled ? await skillStore.disableSkill(skillId) : await skillStore.enableSkill(skillId) } catch (error) { actionError.value = error instanceof Error ? error.message : '状态更新失败' }
}
async function uninstall(skillId: string, name: string) {
if (!confirm(`确定卸载 Skill“${name}”吗?`)) return
try { await skillStore.uninstallSkill(skillId) } catch (error) { actionError.value = error instanceof Error ? error.message : '卸载失败' }
}
</script>
<template>
<section class="feature-page">
<header class="feature-header"><div><h1>Skill 管理</h1><p>查看工作流使用的 Tool权限检索配置和模型要求</p></div><button class="button-primary" @click="install">安装 Skill</button></header>
<div v-if="skillStore.error || actionError" class="error-banner">{{ skillStore.error || actionError }}</div>
<div v-if="skillStore.selectedSkill" class="panel detail-panel">
<div class="detail-head"><div><span class="badge" :class="{ success: skillStore.selectedSkill.status === 'ready', error: skillStore.selectedSkill.status === 'error', warning: skillStore.selectedSkill.status.includes('missing') }">{{ skillStore.selectedSkill.status }}</span><h2>{{ skillStore.selectedSkill.icon }} {{ skillStore.selectedSkill.name }}</h2><p class="muted">v{{ skillStore.selectedSkill.version }} · {{ skillStore.selectedSkill.author || '未知作者' }}</p></div><div class="inline-actions"><button class="button-secondary" @click="toggle(skillStore.selectedSkill.skill_id, skillStore.selectedSkill.enabled)">{{ skillStore.selectedSkill.enabled ? '停用' : '启用' }}</button><button class="button-danger" @click="uninstall(skillStore.selectedSkill.skill_id, skillStore.selectedSkill.name)">卸载</button></div></div>
<p class="description">{{ skillStore.selectedSkill.description }}</p>
<div class="detail-grid"><div><h3>工具</h3><div class="tag-list"><span v-for="tool in skillStore.selectedSkill.tools" :key="tool" class="badge info">{{ tool }}</span></div></div><div><h3>权限</h3><div class="tag-list"><span v-for="permission in skillStore.selectedSkill.permissions" :key="permission" class="badge warning">{{ permission }}</span></div></div><div><h3>检索配置</h3><pre>{{ JSON.stringify(skillStore.selectedSkill.retrieval_config, null, 2) }}</pre></div><div><h3>模型能力</h3><div class="tag-list"><span v-for="cap in skillStore.selectedSkill.model_requirements?.capabilities" :key="cap" class="badge">{{ cap }}</span></div></div></div>
<div v-if="skillStore.selectedSkill.missing_dependencies?.length" class="error-banner dependencies">缺失依赖{{ skillStore.selectedSkill.missing_dependencies.join('') }}</div>
</div>
<div v-else class="feature-grid"><article v-for="skill in skillStore.skills" :key="skill.skill_id" class="item-card extension-card" @click="skillStore.selectSkill(skill.skill_id)"><div class="extension-title"><span class="icon">{{ skill.icon || '⚡' }}</span><div><strong>{{ skill.name }}</strong><p>v{{ skill.version }}</p></div><span class="badge" :class="{ success: skill.status === 'ready', warning: skill.status === 'dependency_missing' }">{{ skill.status }}</span></div><p class="muted">{{ skill.description }}</p><div class="tag-list"><span v-for="permission in skill.permissions.slice(0, 3)" :key="permission" class="badge">{{ permission }}</span></div></article></div>
</section>
</template>
<style scoped>
.detail-head, .extension-title { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-md); }
.detail-head h2 { margin-top: var(--space-sm); }
.description { margin: var(--space-xl) 0; line-height: var(--line-height-relaxed); }
.detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--space-xl); }
.detail-grid h3 { margin-bottom: var(--space-sm); }
pre { padding: var(--space-md); border-radius: var(--radius-md); background: var(--color-background-secondary); }
.dependencies { margin: var(--space-xl) 0 0; }
.extension-card { cursor: pointer; }
.extension-card > p { margin: var(--space-md) 0; }
.extension-title { align-items: center; }
.extension-title .icon { font-size: 28px; }
.extension-title p { color: var(--color-text-tertiary); font-size: var(--font-size-xs); }
</style>
+2 -1
View File
@@ -20,7 +20,8 @@ function toPlugin(plugin: ApiPlugin): Plugin {
description: manifest.description, description: manifest.description,
status: plugin.status, status: plugin.status,
enabled: plugin.enabled, enabled: plugin.enabled,
permissions: plugin.granted_permissions, permissions: manifest.permissions,
granted_permissions: plugin.granted_permissions,
contributions, contributions,
backend_type: manifest.backend.type, backend_type: manifest.backend.type,
transport: manifest.backend.transport, transport: manifest.backend.transport,
+31 -14
View File
@@ -1,12 +1,13 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import type { Plugin } from '@/contracts' import type { Plugin } from '@/contracts'
import { mockPlugins } from '@/services/pluginService' import * as pluginService from '@/services/pluginService'
export const usePluginStore = defineStore('plugin', () => { export const usePluginStore = defineStore('plugin', () => {
const plugins = ref<Plugin[]>(mockPlugins) const plugins = ref<Plugin[]>(pluginService.mockPlugins)
const selectedPluginId = ref<string | null>(null) const selectedPluginId = ref<string | null>(null)
const isLoading = ref(false) const isLoading = ref(false)
const error = ref<string | null>(null)
const selectedPlugin = computed(() => const selectedPlugin = computed(() =>
plugins.value.find((p) => p.plugin_id === selectedPluginId.value) || null plugins.value.find((p) => p.plugin_id === selectedPluginId.value) || null
@@ -19,8 +20,10 @@ export const usePluginStore = defineStore('plugin', () => {
async function loadPlugins() { async function loadPlugins() {
isLoading.value = true isLoading.value = true
try { try {
const { listPlugins } = await import('@/services/pluginService') plugins.value = await pluginService.listPlugins()
plugins.value = await listPlugins() error.value = null
} catch (reason) {
error.value = reason instanceof Error ? reason.message : 'Plugin 加载失败'
} finally { } finally {
isLoading.value = false isLoading.value = false
} }
@@ -30,23 +33,34 @@ export const usePluginStore = defineStore('plugin', () => {
selectedPluginId.value = pluginId selectedPluginId.value = pluginId
} }
async function installPlugin(packagePath: string) {
const installed = await pluginService.installPlugin(packagePath)
const index = plugins.value.findIndex((plugin) => plugin.plugin_id === installed.plugin_id)
if (index >= 0) plugins.value[index] = installed
else plugins.value.unshift(installed)
selectedPluginId.value = installed.plugin_id
}
async function grantPermissions(pluginId: string, permissions: string[]) {
const updated = await pluginService.grantPluginPermissions(pluginId, permissions)
const index = plugins.value.findIndex((plugin) => plugin.plugin_id === pluginId)
if (index >= 0) plugins.value[index] = updated
}
async function enablePlugin(pluginId: string) { async function enablePlugin(pluginId: string) {
const plugin = plugins.value.find((p) => p.plugin_id === pluginId) const updated = await pluginService.enablePlugin(pluginId)
if (plugin) { const index = plugins.value.findIndex((plugin) => plugin.plugin_id === pluginId)
plugin.enabled = true if (index >= 0) plugins.value[index] = updated
plugin.status = 'ready'
}
} }
async function disablePlugin(pluginId: string) { async function disablePlugin(pluginId: string) {
const plugin = plugins.value.find((p) => p.plugin_id === pluginId) const updated = await pluginService.disablePlugin(pluginId)
if (plugin) { const index = plugins.value.findIndex((plugin) => plugin.plugin_id === pluginId)
plugin.enabled = false if (index >= 0) plugins.value[index] = updated
plugin.status = 'disabled'
}
} }
async function uninstallPlugin(pluginId: string) { async function uninstallPlugin(pluginId: string) {
await pluginService.uninstallPlugin(pluginId)
const idx = plugins.value.findIndex((p) => p.plugin_id === pluginId) const idx = plugins.value.findIndex((p) => p.plugin_id === pluginId)
if (idx > -1) plugins.value.splice(idx, 1) if (idx > -1) plugins.value.splice(idx, 1)
if (selectedPluginId.value === pluginId) selectedPluginId.value = null if (selectedPluginId.value === pluginId) selectedPluginId.value = null
@@ -60,8 +74,11 @@ export const usePluginStore = defineStore('plugin', () => {
readyPlugins, readyPlugins,
errorPlugins, errorPlugins,
isLoading, isLoading,
error,
loadPlugins, loadPlugins,
selectPlugin, selectPlugin,
installPlugin,
grantPermissions,
enablePlugin, enablePlugin,
disablePlugin, disablePlugin,
uninstallPlugin, uninstallPlugin,
+24 -14
View File
@@ -1,12 +1,13 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import type { Skill } from '@/contracts' import type { Skill } from '@/contracts'
import { mockSkills } from '@/services/skillService' import * as skillService from '@/services/skillService'
export const useSkillStore = defineStore('skill', () => { export const useSkillStore = defineStore('skill', () => {
const skills = ref<Skill[]>(mockSkills) const skills = ref<Skill[]>(skillService.mockSkills)
const selectedSkillId = ref<string | null>(null) const selectedSkillId = ref<string | null>(null)
const isLoading = ref(false) const isLoading = ref(false)
const error = ref<string | null>(null)
const selectedSkill = computed(() => const selectedSkill = computed(() =>
skills.value.find((s) => s.skill_id === selectedSkillId.value) || null skills.value.find((s) => s.skill_id === selectedSkillId.value) || null
@@ -19,8 +20,10 @@ export const useSkillStore = defineStore('skill', () => {
async function loadSkills() { async function loadSkills() {
isLoading.value = true isLoading.value = true
try { try {
const { listSkills } = await import('@/services/skillService') skills.value = await skillService.listSkills()
skills.value = await listSkills() error.value = null
} catch (reason) {
error.value = reason instanceof Error ? reason.message : 'Skill 加载失败'
} finally { } finally {
isLoading.value = false isLoading.value = false
} }
@@ -30,23 +33,28 @@ export const useSkillStore = defineStore('skill', () => {
selectedSkillId.value = skillId selectedSkillId.value = skillId
} }
async function installSkill(packagePath: string) {
const installed = await skillService.installSkill(packagePath)
const index = skills.value.findIndex((skill) => skill.skill_id === installed.skill_id)
if (index >= 0) skills.value[index] = installed
else skills.value.unshift(installed)
selectedSkillId.value = installed.skill_id
}
async function enableSkill(skillId: string) { async function enableSkill(skillId: string) {
const skill = skills.value.find((s) => s.skill_id === skillId) const updated = await skillService.enableSkill(skillId)
if (skill) { const index = skills.value.findIndex((skill) => skill.skill_id === skillId)
skill.enabled = true if (index >= 0) skills.value[index] = updated
skill.status = 'ready'
}
} }
async function disableSkill(skillId: string) { async function disableSkill(skillId: string) {
const skill = skills.value.find((s) => s.skill_id === skillId) const updated = await skillService.disableSkill(skillId)
if (skill) { const index = skills.value.findIndex((skill) => skill.skill_id === skillId)
skill.enabled = false if (index >= 0) skills.value[index] = updated
skill.status = 'disabled'
}
} }
async function uninstallSkill(skillId: string) { async function uninstallSkill(skillId: string) {
await skillService.uninstallSkill(skillId)
const idx = skills.value.findIndex((s) => s.skill_id === skillId) const idx = skills.value.findIndex((s) => s.skill_id === skillId)
if (idx > -1) skills.value.splice(idx, 1) if (idx > -1) skills.value.splice(idx, 1)
if (selectedSkillId.value === skillId) selectedSkillId.value = null if (selectedSkillId.value === skillId) selectedSkillId.value = null
@@ -60,8 +68,10 @@ export const useSkillStore = defineStore('skill', () => {
installedSkills, installedSkills,
readySkills, readySkills,
isLoading, isLoading,
error,
loadSkills, loadSkills,
selectSkill, selectSkill,
installSkill,
enableSkill, enableSkill,
disableSkill, disableSkill,
uninstallSkill, uninstallSkill,