feat(plugins): 依赖互调 + SQLite 存储,契约 0.3.0
This commit is contained in:
@@ -5,8 +5,10 @@ import {
|
||||
LuArrowLeft,
|
||||
LuCheck,
|
||||
LuCpu,
|
||||
LuDatabase,
|
||||
LuFolderOpen,
|
||||
LuKeyRound,
|
||||
LuLink2,
|
||||
LuLogOut,
|
||||
LuPlus,
|
||||
LuPuzzle,
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
LuSparkles,
|
||||
LuTerminal,
|
||||
LuTextQuote,
|
||||
LuTrash2,
|
||||
LuX
|
||||
} from 'react-icons/lu'
|
||||
import type { IconType } from 'react-icons'
|
||||
@@ -23,6 +26,7 @@ import type {
|
||||
CustomProviderConfig,
|
||||
ModelCatalog,
|
||||
ModelMeta,
|
||||
PluginDepStatus,
|
||||
PluginInfo,
|
||||
ProviderCatalogEntry,
|
||||
ResourceDetails,
|
||||
@@ -631,6 +635,124 @@ function ModelSettings({
|
||||
)
|
||||
}
|
||||
|
||||
/** 依赖状态展示元数据(状态色对齐状态胶囊语义:绿=可用 / 橙=待处理 / 红=失败) */
|
||||
const DEP_STATUS_META: Record<PluginDepStatus['status'], { label: string; cls: string }> = {
|
||||
ok: { label: '可用', cls: 'text-green' },
|
||||
missing: { label: '未安装', cls: 'text-orange' },
|
||||
disabled: { label: '已停用', cls: 'text-orange' },
|
||||
version: { label: '版本不符', cls: 'text-orange' },
|
||||
error: { label: '装载失败', cls: 'text-red' }
|
||||
}
|
||||
|
||||
/** 依赖关系 Tooltip(管理页行内悬停):展示每个依赖的 id / 版本范围 / 必需性 / 实时状态。
|
||||
* 必需依赖缺失或停用时本插件不可用——错误详情见行内红字,去启用在对应插件的行上操作。 */
|
||||
function PluginDepsTip({ deps }: { deps: PluginDepStatus[] }): ReactNode {
|
||||
return (
|
||||
<span className="group relative inline-flex" onClick={(e) => e.stopPropagation()}>
|
||||
<LuLink2 size={11} className="cursor-help text-ink-3 transition-colors group-hover:text-ink-2" />
|
||||
<span className="pointer-events-none invisible absolute left-1/2 top-full z-50 mt-1.5 flex -translate-x-1/2 flex-col gap-1 rounded-card border border-line bg-surface px-2.5 py-2 opacity-0 shadow-overlay transition-opacity duration-150 group-hover:visible group-hover:opacity-100">
|
||||
<span className="text-[10px] font-medium uppercase tracking-wide text-ink-3">依赖</span>
|
||||
{deps.map((d) => (
|
||||
<span key={`${d.id}:${d.required}`} className="flex items-center gap-1.5 whitespace-nowrap text-[11px] text-ink">
|
||||
<span className="font-mono">{d.id}</span>
|
||||
<span className="font-mono text-ink-3">{d.range}</span>
|
||||
<span className="ml-auto pl-3 text-ink-3">{d.required ? '必需' : '可选'}</span>
|
||||
<span className={`w-14 text-right ${DEP_STATUS_META[d.status].cls}`}>{DEP_STATUS_META[d.status].label}</span>
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/** 插件存储区(插件二级页):机器可读 schema 展示 + 数据目录/清除数据动作。
|
||||
* 数据在 userData/plugin-data/<id>/data.db(每插件一库,宿主共享 worker 持有连接)。 */
|
||||
function PluginStorageSection({
|
||||
info,
|
||||
onOpenDir,
|
||||
onClearData
|
||||
}: {
|
||||
info: PluginInfo
|
||||
onOpenDir: () => void
|
||||
onClearData: () => void
|
||||
}): ReactNode {
|
||||
const [confirming, setConfirming] = useState(false)
|
||||
const schema = info.storageSchema
|
||||
return (
|
||||
<div className="rounded-card border border-line">
|
||||
<div className="flex items-center gap-2 border-b border-line px-3 py-2">
|
||||
<LuDatabase size={13} className="shrink-0 text-ink-3" />
|
||||
<span className="text-[12px] font-medium text-ink">存储区</span>
|
||||
<span className="min-w-0 truncate text-[10.5px] text-ink-3" title={`userData/plugin-data/${info.manifest.id}`}>
|
||||
SQLite · 插件私有
|
||||
</span>
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenDir}
|
||||
className="flex items-center gap-1 rounded-control border border-line-strong px-2 py-1 text-[11px] text-ink transition-colors hover:bg-hover"
|
||||
>
|
||||
<LuFolderOpen size={11} />
|
||||
数据目录
|
||||
</button>
|
||||
{confirming ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setConfirming(false)
|
||||
onClearData()
|
||||
}}
|
||||
onMouseLeave={() => setConfirming(false)}
|
||||
className="flex items-center gap-1 rounded-control bg-red px-2 py-1 text-[11px] font-medium text-white transition-opacity hover:opacity-90"
|
||||
>
|
||||
<LuTrash2 size={11} />
|
||||
再点一次确认
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfirming(true)}
|
||||
className="flex items-center gap-1 rounded-control border border-line-strong px-2 py-1 text-[11px] text-ink-3 transition-colors hover:bg-hover hover:text-red"
|
||||
>
|
||||
<LuTrash2 size={11} />
|
||||
清除数据
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{schema && (
|
||||
<div className="flex flex-col gap-2.5 px-3 py-2.5">
|
||||
{schema.kv && (
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="shrink-0 rounded-[4px] bg-hover px-1.5 py-0.5 font-mono text-[10.5px] text-ink-2">kv</span>
|
||||
<span className="min-w-0 text-[11px] leading-4 text-ink-3">{schema.kv.description}</span>
|
||||
</div>
|
||||
)}
|
||||
{schema.tables.map((t) => (
|
||||
<div key={t.name}>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="shrink-0 rounded-[4px] bg-hover px-1.5 py-0.5 font-mono text-[10.5px] text-ink-2">{t.name}</span>
|
||||
<span className="min-w-0 text-[11px] leading-4 text-ink-3">{t.description}</span>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-col gap-0.5 pl-2">
|
||||
{t.columns.map((c) => (
|
||||
<div key={c.name} className="flex items-baseline gap-2 text-[11px] leading-4">
|
||||
<span className="w-28 shrink-0 truncate font-mono text-ink-2" title={c.name}>
|
||||
{c.name}
|
||||
</span>
|
||||
<span className="w-16 shrink-0 font-mono text-ink-3">{c.type}</span>
|
||||
<span className="min-w-0 text-ink-3">{c.description}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 插件页:内置 / 全局 / 项目三个作用域统一管理(交互对齐技能页),行内启停开关;单一事实源在主进程,listChanged 驱动刷新 */
|
||||
function PluginsPanel({
|
||||
togglingId,
|
||||
@@ -733,7 +855,7 @@ function PluginsPanel({
|
||||
(a.manifest.order ?? 100) - (b.manifest.order ?? 100) || a.manifest.id.localeCompare(b.manifest.id)
|
||||
)
|
||||
.map((p) => {
|
||||
const openSettings = p.hasSettings && p.enabled
|
||||
const openSettings = p.enabled && (p.hasSettings || Boolean(p.storageSchema))
|
||||
return (
|
||||
<div
|
||||
key={p.manifest.id}
|
||||
@@ -760,6 +882,7 @@ function PluginsPanel({
|
||||
<span className="rounded-control bg-hover px-1.5 py-0.5 text-[10px] text-ink-3">
|
||||
{scope === 'builtin' ? '内置' : scope === 'user' ? '全局' : '项目'} v{p.manifest.version}
|
||||
</span>
|
||||
{p.dependencies.length > 0 && <PluginDepsTip deps={p.dependencies} />}
|
||||
</div>
|
||||
{p.manifest.description && (
|
||||
<div className="mt-1 text-[12px] leading-5 text-ink-3">{p.manifest.description}</div>
|
||||
@@ -1511,7 +1634,10 @@ export function SettingsModal({
|
||||
(() => {
|
||||
const pid = tab.slice(7)
|
||||
const schema = settingSchemas[pid]
|
||||
if (!schema || schema.items.length === 0) {
|
||||
const info = plugins.find((p) => p.manifest.id === pid)
|
||||
const hasForm = Boolean(schema && schema.items.length > 0)
|
||||
const hasStorage = Boolean(info?.storageSchema)
|
||||
if (!hasForm && !hasStorage) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-[12.5px] text-ink-3">
|
||||
该插件未提供配置项(或插件已停用 / 移除)
|
||||
@@ -1519,13 +1645,22 @@ export function SettingsModal({
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="h-full min-h-0 overflow-y-auto pr-1">
|
||||
<PluginSettingsForm
|
||||
schema={schema}
|
||||
values={settingValues[pid] ?? {}}
|
||||
onChange={(key, value) => handleSetSettingValue(pid, key, value)}
|
||||
onAction={(key) => void window.api.pluginsSettingsAction(pid, key)}
|
||||
/>
|
||||
<div className="flex h-full min-h-0 flex-col gap-4 overflow-y-auto pb-2 pr-1">
|
||||
{hasForm && (
|
||||
<PluginSettingsForm
|
||||
schema={schema}
|
||||
values={settingValues[pid] ?? {}}
|
||||
onChange={(key, value) => handleSetSettingValue(pid, key, value)}
|
||||
onAction={(key) => void window.api.pluginsSettingsAction(pid, key)}
|
||||
/>
|
||||
)}
|
||||
{info && hasStorage && (
|
||||
<PluginStorageSection
|
||||
info={info}
|
||||
onOpenDir={() => void window.api.pluginsOpenDataDir(pid).catch(() => undefined)}
|
||||
onClearData={() => run('已清除插件数据', () => window.api.pluginsClearData(pid))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
@@ -131,6 +131,8 @@ declare global {
|
||||
onPluginsSettingsSchemas: (callback: (schemas: PluginSettingsSchemas) => void) => () => void
|
||||
onPluginsSettingsValues: (callback: (values: PluginSettingsValuesMap) => void) => () => void
|
||||
pluginsSetEnabled: (id: string, enabled: boolean) => Promise<void>
|
||||
pluginsOpenDataDir: (id: string) => Promise<boolean>
|
||||
pluginsClearData: (id: string) => Promise<void>
|
||||
pluginsReload: () => Promise<void>
|
||||
pluginsOpenView: (view: string, params?: unknown) => Promise<void>
|
||||
pluginsSetStatus: (pluginId: string, itemId: string, item: unknown) => Promise<void>
|
||||
|
||||
Reference in New Issue
Block a user