feat: 设置「插件」页落地——外置插件按来源分组(商店/项目/全局)展示与启停
This commit is contained in:
@@ -37,6 +37,8 @@ export const EXTERNAL_PLUGINS_ENABLED = !app.isPackaged || process.env.ASO_EXTER
|
||||
interface ResolvedPlugin {
|
||||
manifest: PluginManifest
|
||||
source: 'builtin' | 'external'
|
||||
/** 外置来源目录类别(管理页分组展示用) */
|
||||
origin?: 'user' | 'project' | 'global'
|
||||
/** 外置插件根目录(plugin:// 映射根) */
|
||||
dir?: string
|
||||
/** 内置:第一方工厂 / 渲染层组件 key */
|
||||
@@ -145,10 +147,12 @@ class PluginHost {
|
||||
}
|
||||
|
||||
if (EXTERNAL_PLUGINS_ENABLED) {
|
||||
const roots = [this.externalRoot]
|
||||
if (this.projectExtensionsDir) roots.push(this.projectExtensionsDir)
|
||||
roots.push(join(getAgentDir(), 'extensions'))
|
||||
for (const root of roots) {
|
||||
const roots: Array<{ root: string; origin: 'user' | 'project' | 'global' }> = [
|
||||
{ root: this.externalRoot, origin: 'user' }
|
||||
]
|
||||
if (this.projectExtensionsDir) roots.push({ root: this.projectExtensionsDir, origin: 'project' })
|
||||
roots.push({ root: join(getAgentDir(), 'extensions'), origin: 'global' })
|
||||
for (const { root, origin } of roots) {
|
||||
if (!existsSync(root)) continue
|
||||
let entries: import('node:fs').Dirent[]
|
||||
try {
|
||||
@@ -177,11 +181,12 @@ class PluginHost {
|
||||
manifest,
|
||||
source: 'external',
|
||||
dir,
|
||||
origin,
|
||||
error: `需要契约版本 >= ${manifest.minContract}(当前 ${HARNESS_CONTRACT_VERSION})`
|
||||
})
|
||||
continue
|
||||
}
|
||||
byId.set(manifest.id, { manifest, source: 'external', dir })
|
||||
byId.set(manifest.id, { manifest, source: 'external', dir, origin })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,6 +280,7 @@ class PluginHost {
|
||||
.map((p) => ({
|
||||
manifest: p.manifest,
|
||||
source: p.source,
|
||||
origin: p.origin,
|
||||
enabled: this.isEnabled(p.manifest.id),
|
||||
hasMain: Boolean(p.builtinFactory ?? p.externalFactory),
|
||||
hasPage: Boolean(p.source === 'external' && p.manifest.entry?.page),
|
||||
|
||||
@@ -53,16 +53,12 @@ const NAV: Array<{ group: string; items: Array<{ id: Tab; label: string; icon: I
|
||||
}
|
||||
]
|
||||
|
||||
const PLACEHOLDER: Record<string, string> = {
|
||||
plugins: '安装扩展插件,为 Agent 增加新的工具与界面能力。'
|
||||
}
|
||||
|
||||
const TAB_META: Record<Tab, { title: string; desc: string }> = {
|
||||
general: { title: '常规', desc: '主题色等外观偏好。' },
|
||||
corePlugins: { title: '核心插件', desc: '内置的第一方功能插件,官方维护' },
|
||||
corePlugins: { title: '核心插件', desc: '内置的第一方功能插件,官方维护' },
|
||||
model: { title: '模型设置', desc: '' },
|
||||
prompt: { title: '系统提示词', desc: '' },
|
||||
plugins: { title: '插件', desc: PLACEHOLDER.plugins },
|
||||
prompt: { title: '系统提示词', desc: '' },
|
||||
plugins: { title: '插件', desc: '第三方扩展插件(商店 / 项目 / 全局目录),按需启停。' },
|
||||
skills: { title: '技能', desc: 'SKILL.md 技能包,Agent 自动按需加载。' },
|
||||
commands: { title: '命令', desc: '聊天中 / 触发的自定义命令模板。' }
|
||||
}
|
||||
@@ -685,6 +681,80 @@ function CorePluginPanel({
|
||||
)
|
||||
}
|
||||
|
||||
/** 第三方插件页:外置插件按来源分组(商店 / 项目 .pi/extensions / 全局扩展目录),启停机制与核心插件相同 */
|
||||
function ExternalPluginPanel({
|
||||
togglingId,
|
||||
onToggle,
|
||||
devBuild
|
||||
}: {
|
||||
togglingId: string | null
|
||||
onToggle: (plugin: PluginInfo, next: boolean) => void
|
||||
/** 开发版构建(第三方装载开关开启);用户版展示关闭提示 */
|
||||
devBuild: boolean
|
||||
}): ReactNode {
|
||||
const external = usePluginList().filter((p) => p.source === 'external')
|
||||
const groups = [
|
||||
{ label: '商店插件', hint: 'userData/plugins', items: external.filter((p) => p.origin === 'user') },
|
||||
{ label: '项目插件', hint: '.pi/extensions(当前工作区)', items: external.filter((p) => p.origin === 'project') },
|
||||
{ label: '全局插件', hint: '~/.pi/agent/extensions', items: external.filter((p) => p.origin === 'global') }
|
||||
].filter((g) => g.items.length > 0)
|
||||
|
||||
if (groups.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">
|
||||
{devBuild ? '暂无第三方插件' : '第三方插件装载已关闭(用户版构建)'}
|
||||
</div>
|
||||
{devBuild && (
|
||||
<div className="text-[11.5px] text-ink-3">把含 manifest.json 的插件目录放入上方任一来源即可自动识别</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{groups.map((group) => (
|
||||
<div key={group.label}>
|
||||
<div className="flex items-baseline gap-1.5 pb-1.5">
|
||||
<span className="text-[11.5px] font-medium text-ink-2">{group.label}</span>
|
||||
<span className="font-mono text-[10.5px] text-ink-3">{group.hint}</span>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{group.items
|
||||
.slice()
|
||||
.sort((a, b) => 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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/** 系统提示词预览:整段 pi 提示词是大文本,memo 化避免弹窗内任意状态变化触发重渲染;挂载耗时靠过渡提交兜住 */
|
||||
const PromptPreview = memo(function PromptPreview({ loading, text }: { loading: boolean; text: string }): ReactNode {
|
||||
return (
|
||||
@@ -773,8 +843,8 @@ export function SettingsModal({
|
||||
.finally(() => setBusy(false))
|
||||
}
|
||||
|
||||
/** 切换核心插件启停:主进程持久化后重建运行时;列表经 plugins:listChanged 自动刷新 */
|
||||
const toggleCorePlugin = (plugin: PluginInfo, next: boolean): void => {
|
||||
/** 切换插件启停(内置/外置同机制):主进程持久化后重建运行时;列表经 plugins:listChanged 自动刷新 */
|
||||
const togglePlugin = (plugin: PluginInfo, next: boolean): void => {
|
||||
if (streaming) {
|
||||
setNotice({ kind: 'err', text: 'Agent 正在回复中,请先停止或等待完成后再切换(切换会重建运行时)' })
|
||||
return
|
||||
@@ -984,7 +1054,11 @@ export function SettingsModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'corePlugins' && <CorePluginPanel togglingId={togglingId} onToggle={toggleCorePlugin} />}
|
||||
{tab === 'corePlugins' && <CorePluginPanel togglingId={togglingId} onToggle={togglePlugin} />}
|
||||
|
||||
{tab === 'plugins' && (
|
||||
<ExternalPluginPanel togglingId={togglingId} onToggle={togglePlugin} devBuild={import.meta.env.DEV} />
|
||||
)}
|
||||
|
||||
{tab === 'prompt' && (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
@@ -1093,7 +1167,7 @@ export function SettingsModal({
|
||||
/>
|
||||
)}
|
||||
|
||||
{tab !== 'general' && tab !== 'corePlugins' && tab !== 'model' && tab !== 'prompt' && tab !== 'skills' && tab !== 'commands' && (
|
||||
{tab !== 'general' && tab !== 'corePlugins' && tab !== 'plugins' && tab !== 'model' && tab !== 'prompt' && tab !== 'skills' && tab !== 'commands' && (
|
||||
<ComingSoon icon={NAV[1].items.find((i) => i.id === tab)?.icon ?? LuPuzzle} label={meta.title} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -367,6 +367,8 @@ import type { PluginManifest, StatusItem, ToolbarButtonDef } from '@aso/plugin-s
|
||||
export interface PluginInfo {
|
||||
manifest: PluginManifest
|
||||
source: 'builtin' | 'external'
|
||||
/** 外置来源目录类别(仅 external):user=商店目录 / project=项目 .pi/extensions / global=全局扩展目录 */
|
||||
origin?: 'user' | 'project' | 'global'
|
||||
enabled: boolean
|
||||
/** 有主进程工厂(pi 工具/事件/内联 UI 注册能力) */
|
||||
hasMain: boolean
|
||||
|
||||
Reference in New Issue
Block a user