feat: 基础设置新增「核心插件」页——内置插件按需启停
This commit is contained in:
@@ -17,10 +17,20 @@ import {
|
|||||||
LuX
|
LuX
|
||||||
} from 'react-icons/lu'
|
} from 'react-icons/lu'
|
||||||
import type { IconType } from 'react-icons'
|
import type { IconType } from 'react-icons'
|
||||||
import type { CustomProviderConfig, ModelCatalog, ModelMeta, ProviderCatalogEntry, ResourceDetails, SessionState, ThemeMode } from '@shared/types'
|
import type {
|
||||||
|
CustomProviderConfig,
|
||||||
|
ModelCatalog,
|
||||||
|
ModelMeta,
|
||||||
|
PluginInfo,
|
||||||
|
ProviderCatalogEntry,
|
||||||
|
ResourceDetails,
|
||||||
|
SessionState,
|
||||||
|
ThemeMode
|
||||||
|
} from '@shared/types'
|
||||||
import { THEME_MODES, THEME_PRESETS, applyThemeColor } from '../lib/theme'
|
import { THEME_MODES, THEME_PRESETS, applyThemeColor } from '../lib/theme'
|
||||||
|
import { usePluginList } from '../plugins/store'
|
||||||
|
|
||||||
type Tab = 'general' | 'model' | 'prompt' | 'plugins' | 'skills' | 'commands'
|
type Tab = 'general' | 'corePlugins' | 'model' | 'prompt' | 'plugins' | 'skills' | 'commands'
|
||||||
|
|
||||||
/** 左侧分组导航(参考 ZCode 设置页:基础设置 / Agent 能力) */
|
/** 左侧分组导航(参考 ZCode 设置页:基础设置 / Agent 能力) */
|
||||||
const NAV: Array<{ group: string; items: Array<{ id: Tab; label: string; icon: IconType }> }> = [
|
const NAV: Array<{ group: string; items: Array<{ id: Tab; label: string; icon: IconType }> }> = [
|
||||||
@@ -29,7 +39,8 @@ const NAV: Array<{ group: string; items: Array<{ id: Tab; label: string; icon: I
|
|||||||
items: [
|
items: [
|
||||||
{ id: 'general', label: '常规', icon: LuSettings2 },
|
{ id: 'general', label: '常规', icon: LuSettings2 },
|
||||||
{ id: 'model', label: '模型设置', icon: LuCpu },
|
{ id: 'model', label: '模型设置', icon: LuCpu },
|
||||||
{ id: 'prompt', label: '系统提示词', icon: LuTextQuote }
|
{ id: 'prompt', label: '系统提示词', icon: LuTextQuote },
|
||||||
|
{ id: 'corePlugins', label: '核心插件', icon: LuPuzzle }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -48,6 +59,7 @@ const PLACEHOLDER: Record<string, string> = {
|
|||||||
|
|
||||||
const TAB_META: Record<Tab, { title: string; desc: string }> = {
|
const TAB_META: Record<Tab, { title: string; desc: string }> = {
|
||||||
general: { title: '常规', desc: '主题色等外观偏好。' },
|
general: { title: '常规', desc: '主题色等外观偏好。' },
|
||||||
|
corePlugins: { title: '核心插件', desc: '内置的第一方功能插件,按需开启或关闭;变更会重建 Agent 运行时,进行中的回合会被中断。' },
|
||||||
model: { title: '模型设置', desc: '' },
|
model: { title: '模型设置', desc: '' },
|
||||||
prompt: { title: '系统提示词', desc: '当前生效的 pi 原生系统提示词(含技能清单注入),只读。' },
|
prompt: { title: '系统提示词', desc: '当前生效的 pi 原生系统提示词(含技能清单注入),只读。' },
|
||||||
plugins: { title: '插件', desc: PLACEHOLDER.plugins },
|
plugins: { title: '插件', desc: PLACEHOLDER.plugins },
|
||||||
@@ -587,6 +599,59 @@ function ModelSettings({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 核心插件页:内置插件列表 + 启停开关(单一事实源在主进程,listChanged 事件驱动刷新) */
|
||||||
|
function CorePluginPanel({
|
||||||
|
togglingId,
|
||||||
|
onToggle
|
||||||
|
}: {
|
||||||
|
/** 正在切换中的插件 id(切换期间全部开关禁用,避免并发重建运行时) */
|
||||||
|
togglingId: string | null
|
||||||
|
onToggle: (plugin: PluginInfo, next: boolean) => void
|
||||||
|
}): ReactNode {
|
||||||
|
// 本页只管核心(内置)插件;外置插件留给「Agent 能力 → 插件」
|
||||||
|
const core = usePluginList().filter((p) => p.source === 'builtin')
|
||||||
|
if (core.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1 rounded-card border border-dashed border-line py-12 text-center">
|
||||||
|
<div className="text-[13px] font-medium text-ink-2">暂无核心插件</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{core
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => (a.manifest.order ?? 100) - (b.manifest.order ?? 100) || a.manifest.id.localeCompare(b.manifest.id))
|
||||||
|
.map((p) => (
|
||||||
|
<div key={p.manifest.id} className="rounded-card border border-line px-3 py-2.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-[12.5px] font-medium text-ink">{p.manifest.name}</span>
|
||||||
|
<span className="rounded-control bg-hover px-1.5 py-0.5 text-[10px] text-ink-3">内置 v{p.manifest.version}</span>
|
||||||
|
<span className="ml-auto text-[11px] text-ink-3">{p.enabled ? '已启用' : '已停用'}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={p.enabled}
|
||||||
|
aria-label={`启用${p.manifest.name}`}
|
||||||
|
disabled={togglingId !== null}
|
||||||
|
onClick={() => onToggle(p, !p.enabled)}
|
||||||
|
className={`relative h-6 w-10 shrink-0 rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${
|
||||||
|
p.enabled ? 'bg-accent' : 'bg-line-strong'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute top-0.5 size-5 rounded-full bg-white shadow transition-[left] ${p.enabled ? 'left-[18px]' : 'left-0.5'}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{p.manifest.description && <div className="mt-1 text-[12px] leading-5 text-ink-3">{p.manifest.description}</div>}
|
||||||
|
{p.error && <div className="mt-1 text-[12px] text-red">装载失败:{p.error}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function SettingsModal({
|
export function SettingsModal({
|
||||||
open,
|
open,
|
||||||
sessionState,
|
sessionState,
|
||||||
@@ -614,6 +679,7 @@ export function SettingsModal({
|
|||||||
const [resources, setResources] = useState<ResourceDetails | null>(null)
|
const [resources, setResources] = useState<ResourceDetails | null>(null)
|
||||||
const [resourcesLoading, setResourcesLoading] = useState(false)
|
const [resourcesLoading, setResourcesLoading] = useState(false)
|
||||||
const [customProviders, setCustomProviders] = useState<CustomProviderConfig[]>([])
|
const [customProviders, setCustomProviders] = useState<CustomProviderConfig[]>([])
|
||||||
|
const [togglingId, setTogglingId] = useState<string | null>(null)
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const refreshCustoms = (): void => {
|
const refreshCustoms = (): void => {
|
||||||
@@ -663,6 +729,22 @@ export function SettingsModal({
|
|||||||
.finally(() => setBusy(false))
|
.finally(() => setBusy(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 切换核心插件启停:主进程持久化后重建运行时;列表经 plugins:listChanged 自动刷新 */
|
||||||
|
const toggleCorePlugin = (plugin: PluginInfo, next: boolean): void => {
|
||||||
|
if (streaming) {
|
||||||
|
setNotice({ kind: 'err', text: 'Agent 正在回复中,请先停止或等待完成后再切换(切换会重建运行时)' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (togglingId) return
|
||||||
|
setTogglingId(plugin.manifest.id)
|
||||||
|
setNotice(null)
|
||||||
|
window.api
|
||||||
|
.pluginsSetEnabled(plugin.manifest.id, next)
|
||||||
|
.then(() => setNotice({ kind: 'ok', text: `已${next ? '启用' : '停用'}「${plugin.manifest.name}」,运行时已重建` }))
|
||||||
|
.catch((err: unknown) => setNotice({ kind: 'err', text: err instanceof Error ? err.message : String(err) }))
|
||||||
|
.finally(() => setTogglingId(null))
|
||||||
|
}
|
||||||
|
|
||||||
const refreshResources = (): void => {
|
const refreshResources = (): void => {
|
||||||
setResourcesLoading(true)
|
setResourcesLoading(true)
|
||||||
window.api
|
window.api
|
||||||
@@ -836,6 +918,15 @@ export function SettingsModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{tab === 'corePlugins' && (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<CorePluginPanel togglingId={togglingId} onToggle={toggleCorePlugin} />
|
||||||
|
<div className="text-[11.5px] leading-5 text-ink-3">
|
||||||
|
开关立即生效(重建 Agent 运行时)。例如停用「浏览器」后,侧栏浏览器面板入口与全部 browser_* 工具将一并停用;重新开启即恢复。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{tab === 'prompt' && (
|
{tab === 'prompt' && (
|
||||||
<div className="flex h-full min-h-0 flex-col">
|
<div className="flex h-full min-h-0 flex-col">
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto whitespace-pre-wrap rounded-card border border-line bg-inset p-3.5 font-mono text-[12px] leading-6 text-ink-2">
|
<div className="min-h-0 flex-1 overflow-y-auto whitespace-pre-wrap rounded-card border border-line bg-inset p-3.5 font-mono text-[12px] leading-6 text-ink-2">
|
||||||
@@ -937,7 +1028,7 @@ export function SettingsModal({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tab !== 'general' && tab !== 'model' && tab !== 'prompt' && tab !== 'skills' && tab !== 'commands' && (
|
{tab !== 'general' && tab !== 'corePlugins' && tab !== 'model' && tab !== 'prompt' && tab !== 'skills' && tab !== 'commands' && (
|
||||||
<ComingSoon icon={NAV[1].items.find((i) => i.id === tab)?.icon ?? LuPuzzle} label={meta.title} />
|
<ComingSoon icon={NAV[1].items.find((i) => i.id === tab)?.icon ?? LuPuzzle} label={meta.title} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user