diff --git a/apps/desktop/src/main/plugins/plugin-host.ts b/apps/desktop/src/main/plugins/plugin-host.ts index 6e4898e..354df93 100644 --- a/apps/desktop/src/main/plugins/plugin-host.ts +++ b/apps/desktop/src/main/plugins/plugin-host.ts @@ -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), diff --git a/apps/desktop/src/renderer/src/components/SettingsModal.tsx b/apps/desktop/src/renderer/src/components/SettingsModal.tsx index 3a1cdc1..9dab9b7 100644 --- a/apps/desktop/src/renderer/src/components/SettingsModal.tsx +++ b/apps/desktop/src/renderer/src/components/SettingsModal.tsx @@ -53,16 +53,12 @@ const NAV: Array<{ group: string; items: Array<{ id: Tab; label: string; icon: I } ] -const PLACEHOLDER: Record = { - plugins: '安装扩展插件,为 Agent 增加新的工具与界面能力。' -} - const TAB_META: Record = { 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 ( +
+
+ {devBuild ? '暂无第三方插件' : '第三方插件装载已关闭(用户版构建)'} +
+ {devBuild && ( +
把含 manifest.json 的插件目录放入上方任一来源即可自动识别
+ )} +
+ ) + } + return ( +
+ {groups.map((group) => ( +
+
+ {group.label} + {group.hint} +
+
+ {group.items + .slice() + .sort((a, b) => a.manifest.id.localeCompare(b.manifest.id)) + .map((p) => ( +
+
+ {p.manifest.name} + v{p.manifest.version} + {p.enabled ? '已启用' : '已停用'} + +
+ {p.manifest.description &&
{p.manifest.description}
} + {p.error &&
装载失败:{p.error}
} +
+ ))} +
+
+ ))} +
+ ) +} /** 系统提示词预览:整段 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({ )} - {tab === 'corePlugins' && } + {tab === 'corePlugins' && } + + {tab === 'plugins' && ( + + )} {tab === 'prompt' && (
@@ -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' && ( i.id === tab)?.icon ?? LuPuzzle} label={meta.title} /> )}
diff --git a/apps/desktop/src/shared/types.ts b/apps/desktop/src/shared/types.ts index 35374e4..0037562 100644 --- a/apps/desktop/src/shared/types.ts +++ b/apps/desktop/src/shared/types.ts @@ -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