feat(web): 插件管理弹窗(启停 / 重载)
- 顶栏新增「插件管理」图标按钮,打开 PluginsDialog - 弹窗列出 pugins/ 下的插件:标题 / 目录名 / 是否启用与已加载 / 平台 label / 能力清单 - 每个插件可启用、停用、重载,底部有「全部重载」 - 启停成功后重新拉 /api/platforms,「添加账号」的平台下拉同步刷新 - types.ts 新增 PluginInfo;App.tsx 把平台列表加载提成 loadPlatforms 复用 - static/ 重新构建(pnpm typecheck + pnpm build 均通过) - 顺带把 .vscode/ 纳入跟踪
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Puzzle, RefreshCw } from 'lucide-react'
|
||||
|
||||
import { api } from '../lib/api'
|
||||
import { toastMessage } from '../lib/utils'
|
||||
import type { PluginInfo } from '../types'
|
||||
import { Badge } from './ui/Badge'
|
||||
import { Button } from './ui/Button'
|
||||
import { Modal } from './ui/Modal'
|
||||
|
||||
interface PluginsResponse {
|
||||
ok?: boolean
|
||||
error?: string
|
||||
plugins?: PluginInfo[]
|
||||
}
|
||||
|
||||
export function PluginsDialog({
|
||||
open,
|
||||
onClose,
|
||||
notify,
|
||||
onChanged,
|
||||
}: {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
notify: (toast: { message: string; tone?: 'info' | 'success' | 'error' }) => void
|
||||
onChanged: () => void
|
||||
}) {
|
||||
const [plugins, setPlugins] = useState<PluginInfo[]>([])
|
||||
const [busy, setBusy] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const pull = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await api<PluginsResponse>('/api/plugins')
|
||||
setPlugins(response.plugins || [])
|
||||
} catch (error) {
|
||||
notify({ message: toastMessage(error), tone: 'error' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [notify])
|
||||
|
||||
useEffect(() => {
|
||||
if (open) void pull()
|
||||
}, [open, pull])
|
||||
|
||||
async function run(name: string, path: string, okText: string) {
|
||||
setBusy(name)
|
||||
try {
|
||||
const response = await api<PluginsResponse>(path, { method: 'POST' })
|
||||
if (response.plugins) setPlugins(response.plugins)
|
||||
if (response.ok === false) {
|
||||
notify({ message: response.error || '插件操作失败', tone: 'error' })
|
||||
} else {
|
||||
notify({ message: okText, tone: 'success' })
|
||||
onChanged()
|
||||
}
|
||||
} catch (error) {
|
||||
notify({ message: toastMessage(error), tone: 'error' })
|
||||
} finally {
|
||||
setBusy('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title="插件管理"
|
||||
subtitle="pugins/<名字>/ 下的平台插件:启停立即生效,改完代码重载即可,不用重启服务"
|
||||
size="lg"
|
||||
footer={
|
||||
<>
|
||||
<span className="mr-auto text-[11px] text-slate-400">
|
||||
共 {plugins.length} 个 · 停用后该平台会从「添加账号」的平台下拉里消失
|
||||
</span>
|
||||
<Button
|
||||
variant="soft"
|
||||
size="sm"
|
||||
icon={<RefreshCw className="size-3.5" />}
|
||||
loading={busy === '*'}
|
||||
onClick={() => void run('*', '/api/plugins/reload', '全部插件已重载')}
|
||||
>
|
||||
全部重载
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="space-y-2.5">
|
||||
{plugins.map((plugin) => (
|
||||
<div key={plugin.name} className="rounded-xl border border-line bg-slate-50/70 px-3.5 py-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Puzzle className="size-4 shrink-0 text-brand-600" />
|
||||
<span className="text-[13px] font-semibold text-slate-900">
|
||||
{plugin.title || plugin.name}
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-400">{plugin.name}</span>
|
||||
<Badge tone={plugin.enabled ? 'success' : 'neutral'} dot>
|
||||
{plugin.enabled ? '已启用' : '已停用'}
|
||||
</Badge>
|
||||
{plugin.enabled && (
|
||||
<Badge tone={plugin.loaded ? 'brand' : 'warning'}>
|
||||
{plugin.loaded ? '已加载' : '未加载'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-slate-500">
|
||||
<span>平台 {plugin.label || '—'}</span>
|
||||
<span>id {plugin.id || '—'}</span>
|
||||
<span>能力 {plugin.capabilities.length ? plugin.capabilities.join(' / ') : '无'}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-2.5 flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={plugin.enabled ? 'secondary' : 'primary'}
|
||||
loading={busy === plugin.name}
|
||||
onClick={() =>
|
||||
void run(
|
||||
plugin.name,
|
||||
`/api/plugins/${plugin.name}/${plugin.enabled ? 'disable' : 'enable'}`,
|
||||
plugin.enabled ? `${plugin.name} 已停用` : `${plugin.name} 已启用`,
|
||||
)
|
||||
}
|
||||
>
|
||||
{plugin.enabled ? '停用' : '启用'}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={!plugin.enabled}
|
||||
loading={busy === plugin.name}
|
||||
onClick={() =>
|
||||
void run(plugin.name, `/api/plugins/${plugin.name}/reload`, `${plugin.name} 已重载`)
|
||||
}
|
||||
>
|
||||
重载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!plugins.length && !loading && (
|
||||
<p className="py-8 text-center text-[12.5px] text-slate-400">
|
||||
pugins/ 下还没有插件。建一个含 pugin.json + main.py 的目录丢进去,刷新即可看到。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user