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